From 05ab12cab8f8bcd4900936e2d75fcaf648f7d17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Tue, 4 Oct 2016 23:15:41 +0200 Subject: [PATCH 01/18] Make a working GET request with Tesla --- lib/dayron/adapters/tesla_adapter.ex | 66 ++++++++ mix.exs | 1 + mix.lock | 9 +- .../dayron/adapters/tesla_adapter_test.exs | 152 ++++++++++++++++++ 4 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 lib/dayron/adapters/tesla_adapter.ex create mode 100644 test/lib/dayron/adapters/tesla_adapter_test.exs diff --git a/lib/dayron/adapters/tesla_adapter.ex b/lib/dayron/adapters/tesla_adapter.ex new file mode 100644 index 0000000..d9be3c1 --- /dev/null +++ b/lib/dayron/adapters/tesla_adapter.ex @@ -0,0 +1,66 @@ +defmodule Dayron.TeslaAdapter do + @moduledoc """ + Makes http requests using Tesla library. + Use this adapter to make http requests to an external Rest API. + + ## Example config + config :my_app, MyApp.Repo, + adapter: Dayron.TeslaAdapter, + url: "https://api.example.com" + """ + @behaviour Dayron.Adapter + + defmodule Client do + @moduledoc """ + A Tesla Client implementation, sending json requests, parsing + json responses to Maps or a List of Maps. Maps keys are also converted to + atoms by default. + """ + use Tesla + + plug Tesla.Middleware.JSON + + adapter Tesla.Adapter.Hackney + end + + @doc """ + Implementation for `Dayron.Adapter.get/3`. + """ + def get(url, headers \\ [], opts \\ []) do + Client.get(url, headers: headers) |> translate_response + end + + @doc """ + Implementation for `Dayron.Adapter.post/4`. + """ + def post(url, body, headers \\ [], opts \\ []) do + Client.post(url, body, headers: headers) |> translate_response + end + + @doc """ + Implementation for `Dayron.Adapter.patch/4`. + """ + def patch(url, body, headers \\ [], opts \\ []) do + Client.patch(url, body, headers: headers) |> translate_response + end + + @doc """ + Implementation for `Dayron.Adapter.delete/3`. + """ + def delete(url, headers \\ [], opts \\ []) do + Client.delete(url, headers: headers) |> translate_response + end + + defp translate_response(%Tesla.Env{} = response) do + {:ok, %Dayron.Response{ + status_code: response.status, + body: response.body |> Poison.decode! |> IO.inspect, + headers: response.headers |> Map.to_list + } + } + end + defp translate_response({:error, response}) do + data = response |> Map.from_struct + {:error, struct(Dayron.ClientError, data)} + end +end diff --git a/mix.exs b/mix.exs index 55a6290..4b5a609 100644 --- a/mix.exs +++ b/mix.exs @@ -45,6 +45,7 @@ defmodule Dayron.Mixfile do [ {:poison, "~> 1.5 or ~> 2.0"}, {:httpoison, "~> 0.8.0"}, + {:tesla, "~> 0.5.0", optional: true}, {:crutches, "~> 1.0.0"}, {:credo, "~> 0.3", only: [:dev, :test]}, {:bypass, "~> 0.1", only: :test}, diff --git a/mix.lock b/mix.lock index c589b7e..4ef92c1 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{"bunt": {:hex, :bunt, "0.1.5", "c378ea1698232597d3778e4b83234dcea4a60e7c28114b0fe53657a2c0d8885e", [:mix], []}, - "bypass": {:hex, :bypass, "0.5.1", "cf3e8a4d376ee1dcd89bf362dfaf1f4bf4a6e19895f52fdc2bafbd8207ce435f", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}, {:cowboy, "~> 1.0", [hex: :cowboy, optional: false]}]}, + "bypass": {:hex, :bypass, "0.5.1", "cf3e8a4d376ee1dcd89bf362dfaf1f4bf4a6e19895f52fdc2bafbd8207ce435f", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]}, "certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:rebar, :make], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, @@ -7,9 +7,9 @@ "crutches": {:hex, :crutches, "1.0.0", "34675f0c88f25a3a6d2a77912194beaece9bf835f9f2f5627027b4a19125aaa6", [:mix], []}, "earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], []}, "ex_doc": {:hex, :ex_doc, "0.11.5", "0dc51cb84f8312162a2313d6c71573a9afa332333d8a332bb12540861b9834db", [:mix], [{:earmark, "~> 0.1.17 or ~> 0.2", [hex: :earmark, optional: true]}]}, - "excoveralls": {:hex, :excoveralls, "0.5.4", "1a6e116bcf980da8b7fe33140c1d7e61aa0a4e51951cadbfacc420c12d2b9b8f", [:mix], [{:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}, {:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}]}, + "excoveralls": {:hex, :excoveralls, "0.5.4", "1a6e116bcf980da8b7fe33140c1d7e61aa0a4e51951cadbfacc420c12d2b9b8f", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]}, "exjsx": {:hex, :exjsx, "3.2.0", "7136cc739ace295fc74c378f33699e5145bead4fdc1b4799822d0287489136fb", [:mix], [{:jsx, "~> 2.6.2", [hex: :jsx, optional: false]}]}, - "hackney": {:hex, :hackney, "1.6.0", "8d1e9440c9edf23bf5e5e2fe0c71de03eb265103b72901337394c840eec679ac", [:rebar3], [{:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:certifi, "0.4.0", [hex: :certifi, optional: false]}]}, + "hackney": {:hex, :hackney, "1.6.0", "8d1e9440c9edf23bf5e5e2fe0c71de03eb265103b72901337394c840eec679ac", [: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]}]}, "httpoison": {:hex, :httpoison, "0.8.3", "b675a3fdc839a0b8d7a285c6b3747d6d596ae70b6ccb762233a990d7289ccae4", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]}, "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, "inch_ex": {:hex, :inch_ex, "0.5.1", "c1c18966c935944cbb2d273796b36e44fab3c54fd59f906ff026a686205b4e14", [:mix], [{:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]}, @@ -19,4 +19,5 @@ "plug": {:hex, :plug, "1.1.4", "2eee0e85ad420db96e075b3191d3764d6fff61422b101dc5b02e9cce99cacfc7", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}]}, "poison": {:hex, :poison, "1.5.2", "560bdfb7449e3ddd23a096929fb9fc2122f709bcc758b2d5d5a5c7d0ea848910", [: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", [:rebar, :make], []}, + "tesla": {:hex, :tesla, "0.5.2", "614297046c638b8c1695e8b6e63e6bffd107c07a67e4cb8cc4bb46598be735f1", [:mix], [{:exjsx, ">= 0.1.0", [hex: :exjsx, optional: true]}, {:hackney, "~> 1.6.0", [hex: :hackney, optional: true]}, {:ibrowse, "~> 4.2", [hex: :ibrowse, optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, optional: true]}]}} diff --git a/test/lib/dayron/adapters/tesla_adapter_test.exs b/test/lib/dayron/adapters/tesla_adapter_test.exs new file mode 100644 index 0000000..8bfea9d --- /dev/null +++ b/test/lib/dayron/adapters/tesla_adapter_test.exs @@ -0,0 +1,152 @@ +defmodule Dayron.TeslaAdapterTest do + use ExUnit.Case, async: true + alias Dayron.TeslaAdapter + + setup do + bypass = Bypass.open + {:ok, bypass: bypass, api_url: "http://localhost:#{bypass.port}"} + end + + test "returns a decoded body for a valid get request", %{bypass: bypass, api_url: api_url} do + Bypass.expect bypass, fn conn -> + assert "/resources/id" == conn.request_path + assert "GET" == conn.method + Plug.Conn.resp(conn, 200, ~s<{"name": "Full Name", "address":{"street": "Elm Street", "zipcode": "88888"}}>) + end + response = TeslaAdapter.get("#{api_url}/resources/id") + assert {:ok, %Dayron.Response{status_code: 200, body: body}} = response + assert body["name"] == "Full Name" + assert body["address"] == %{"street" => "Elm Street", "zipcode" => "88888"} + end + + @tag skip: "TBD" + test "handles response body 'ok'", %{bypass: bypass, api_url: api_url} do + Bypass.expect bypass, fn conn -> + assert "/resources/id" == conn.request_path + assert "GET" == conn.method + Plug.Conn.resp(conn, 200, "ok") + end + response = TeslaAdapter.get("#{api_url}/resources/id") + assert {:ok, %Dayron.Response{status_code: 200, body: body}} = response + assert body == %{} + end + + @tag skip: "TBD" + test "handles invalid json body", %{bypass: bypass, api_url: api_url} do + Bypass.expect bypass, fn conn -> + assert "/resources/id" == conn.request_path + assert "GET" == conn.method + Plug.Conn.resp(conn, 200, "{invalid_json=1}") + end + response = TeslaAdapter.get("#{api_url}/resources/id") + assert {:ok, %Dayron.Response{status_code: 200, body: body}} = response + assert body == "{invalid_json=1}" + end + + @tag skip: "TBD" + test "returns a decoded body for a response list", %{bypass: bypass, api_url: api_url} do + Bypass.expect bypass, fn conn -> + assert "/resources" == conn.request_path + assert "GET" == conn.method + Plug.Conn.resp(conn, 200, ~s<[{"name": "First Resource"}, {"name": "Second Resource"}]>) + end + response = TeslaAdapter.get("#{api_url}/resources") + assert {:ok, %Dayron.Response{status_code: 200, body: body}} = response + [first, second | _t] = body + assert first[:name] == "First Resource" + assert second[:name] == "Second Resource" + end + + @tag skip: "TBD" + test "accepts query parameters and headers", %{bypass: bypass, api_url: api_url} do + Bypass.expect bypass, fn conn -> + assert "/resources" == conn.request_path + assert "q=qu+ery&page=2" == conn.query_string + assert [{"accept", "application/json"} | _] = conn.req_headers + assert "GET" == conn.method + Plug.Conn.resp(conn, 200, "") + end + response = TeslaAdapter.get("#{api_url}/resources", [{"accept", "application/json"}], [params: [{:q, "qu ery"}, {:page, 2}]]) + assert {:ok, %Dayron.Response{status_code: 200, body: _}} = response + end + + @tag skip: "TBD" + test "accepts custom headers", %{bypass: bypass, api_url: api_url} do + Bypass.expect bypass, fn conn -> + assert "/resources/id" == conn.request_path + assert [_a, _b, {"accesstoken", "token"} | _] = conn.req_headers + assert "GET" == conn.method + Plug.Conn.resp(conn, 200, "") + end + response = TeslaAdapter.get("#{api_url}/resources/id", [accesstoken: "token"]) + assert {:ok, %Dayron.Response{status_code: 200, body: _}} = response + end + + @tag skip: "TBD" + test "returns a 404 response", %{bypass: bypass, api_url: api_url} do + Bypass.expect bypass, fn conn -> + assert "/resources/invalid" == conn.request_path + assert "GET" == conn.method + Plug.Conn.resp(conn, 404, "") + end + response = TeslaAdapter.get("#{api_url}/resources/invalid") + assert {:ok, %Dayron.Response{status_code: 404, body: _}} = response + end + + @tag skip: "TBD" + test "returns a 500 error response", %{bypass: bypass, api_url: api_url} do + Bypass.expect bypass, fn conn -> + assert "/resources/server-error" == conn.request_path + assert "GET" == conn.method + Plug.Conn.resp(conn, 500, "") + end + response = TeslaAdapter.get("#{api_url}/resources/server-error") + assert {:ok, %Dayron.Response{status_code: 500, body: _}} = response + end + + @tag skip: "TBD" + test "returns an error for invalid server" do + response = TeslaAdapter.get("http://localhost:0001/resources/error") + assert {:error, %Dayron.ClientError{reason: :econnrefused}} = response + end + + @tag skip: "TBD" + test "returns a decoded body for a valid post request", %{bypass: bypass, api_url: api_url} do + Bypass.expect bypass, fn conn -> + assert "/resources" == conn.request_path + assert [{"accept", "application/json"} | _] = conn.req_headers + assert "POST" == conn.method + Plug.Conn.resp(conn, 201, ~s<{"name": "Full Name", "age": 30}>) + end + response = TeslaAdapter.post("#{api_url}/resources", %{name: "Full Name", age: 30}, [{"accept", "application/json"}]) + assert {:ok, %Dayron.Response{status_code: 201, body: body}} = response + assert body[:name] == "Full Name" + assert body[:age] == 30 + end + + @tag skip: "TBD" + test "returns a decoded body for a valid patch request", %{bypass: bypass, api_url: api_url} do + Bypass.expect bypass, fn conn -> + assert "/resources/id" == conn.request_path + assert [{"accept", "application/json"} | _] = conn.req_headers + assert "PATCH" == conn.method + Plug.Conn.resp(conn, 200, ~s<{"name": "Full Name", "age": 30}>) + end + response = TeslaAdapter.patch("#{api_url}/resources/id", %{name: "Full Name", age: 30}, [{"accept", "application/json"}]) + assert {:ok, %Dayron.Response{status_code: 200, body: body}} = response + assert body[:name] == "Full Name" + assert body[:age] == 30 + end + + @tag skip: "TBD" + test "returns an empty body for a valid delete request", %{bypass: bypass, api_url: api_url} do + Bypass.expect bypass, fn conn -> + assert "/resources/id" == conn.request_path + assert [{"content-type", "application/json"}|_] = conn.req_headers + assert "DELETE" == conn.method + Plug.Conn.resp(conn, 204, "") + end + response = TeslaAdapter.delete("#{api_url}/resources/id", [{"content-type", "application/json"}]) + assert {:ok, %Dayron.Response{status_code: 204, body: nil}} = response + end +end From bd779998fa5e47b6b1b1871d0137954deb648c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Tue, 11 Oct 2016 08:10:11 +0200 Subject: [PATCH 02/18] Use custom JSON decoding due to special requirements --- lib/dayron/adapters/tesla_adapter.ex | 13 ++++++++++--- test/lib/dayron/adapters/tesla_adapter_test.exs | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/dayron/adapters/tesla_adapter.ex b/lib/dayron/adapters/tesla_adapter.ex index d9be3c1..c4d3898 100644 --- a/lib/dayron/adapters/tesla_adapter.ex +++ b/lib/dayron/adapters/tesla_adapter.ex @@ -18,8 +18,6 @@ defmodule Dayron.TeslaAdapter do """ use Tesla - plug Tesla.Middleware.JSON - adapter Tesla.Adapter.Hackney end @@ -54,7 +52,7 @@ defmodule Dayron.TeslaAdapter do defp translate_response(%Tesla.Env{} = response) do {:ok, %Dayron.Response{ status_code: response.status, - body: response.body |> Poison.decode! |> IO.inspect, + body: translate_response_body(response.body), headers: response.headers |> Map.to_list } } @@ -63,4 +61,13 @@ defmodule Dayron.TeslaAdapter do data = response |> Map.from_struct {:error, struct(Dayron.ClientError, data)} end + + defp translate_response_body(body) do + try do + body |> Poison.decode!(keys: :atoms) + rescue + Poison.SyntaxError -> body + end + end + end diff --git a/test/lib/dayron/adapters/tesla_adapter_test.exs b/test/lib/dayron/adapters/tesla_adapter_test.exs index 8bfea9d..1bf8a7d 100644 --- a/test/lib/dayron/adapters/tesla_adapter_test.exs +++ b/test/lib/dayron/adapters/tesla_adapter_test.exs @@ -15,8 +15,8 @@ defmodule Dayron.TeslaAdapterTest do end response = TeslaAdapter.get("#{api_url}/resources/id") assert {:ok, %Dayron.Response{status_code: 200, body: body}} = response - assert body["name"] == "Full Name" - assert body["address"] == %{"street" => "Elm Street", "zipcode" => "88888"} + assert body[:name] == "Full Name" + assert body[:address] == %{street: "Elm Street", zipcode: "88888"} end @tag skip: "TBD" From 9006d2ca514c0efc8f71a4d3d61d11514ccf52dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Tue, 11 Oct 2016 08:14:06 +0200 Subject: [PATCH 03/18] Add note about using Tesla for JSON decoding --- lib/dayron/adapters/tesla_adapter.ex | 6 ++++++ test/lib/dayron/adapters/tesla_adapter_test.exs | 3 --- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/dayron/adapters/tesla_adapter.ex b/lib/dayron/adapters/tesla_adapter.ex index c4d3898..7ae298e 100644 --- a/lib/dayron/adapters/tesla_adapter.ex +++ b/lib/dayron/adapters/tesla_adapter.ex @@ -18,6 +18,11 @@ defmodule Dayron.TeslaAdapter do """ use Tesla + # TODO: If the Tesla JSON decoding was a bit more flexible, properly + # returning errors along with raw output and perhaps allowing a force + # flag to ignore content-type header contents, we could use that instead + # of our own custom decoding. Perhaps a pull request would do it.. + adapter Tesla.Adapter.Hackney end @@ -62,6 +67,7 @@ defmodule Dayron.TeslaAdapter do {:error, struct(Dayron.ClientError, data)} end + defp translate_response_body("ok"), do: %{} defp translate_response_body(body) do try do body |> Poison.decode!(keys: :atoms) diff --git a/test/lib/dayron/adapters/tesla_adapter_test.exs b/test/lib/dayron/adapters/tesla_adapter_test.exs index 1bf8a7d..ba0f916 100644 --- a/test/lib/dayron/adapters/tesla_adapter_test.exs +++ b/test/lib/dayron/adapters/tesla_adapter_test.exs @@ -19,7 +19,6 @@ defmodule Dayron.TeslaAdapterTest do assert body[:address] == %{street: "Elm Street", zipcode: "88888"} end - @tag skip: "TBD" test "handles response body 'ok'", %{bypass: bypass, api_url: api_url} do Bypass.expect bypass, fn conn -> assert "/resources/id" == conn.request_path @@ -31,7 +30,6 @@ defmodule Dayron.TeslaAdapterTest do assert body == %{} end - @tag skip: "TBD" test "handles invalid json body", %{bypass: bypass, api_url: api_url} do Bypass.expect bypass, fn conn -> assert "/resources/id" == conn.request_path @@ -43,7 +41,6 @@ defmodule Dayron.TeslaAdapterTest do assert body == "{invalid_json=1}" end - @tag skip: "TBD" test "returns a decoded body for a response list", %{bypass: bypass, api_url: api_url} do Bypass.expect bypass, fn conn -> assert "/resources" == conn.request_path From c20fbe6b10e6f23b1736c1b8439e6f3e52af20cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Tue, 11 Oct 2016 08:20:20 +0200 Subject: [PATCH 04/18] Support query parameters for Tesla --- lib/dayron/adapters/tesla_adapter.ex | 3 ++- test/lib/dayron/adapters/tesla_adapter_test.exs | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dayron/adapters/tesla_adapter.ex b/lib/dayron/adapters/tesla_adapter.ex index 7ae298e..5dddc24 100644 --- a/lib/dayron/adapters/tesla_adapter.ex +++ b/lib/dayron/adapters/tesla_adapter.ex @@ -30,7 +30,8 @@ defmodule Dayron.TeslaAdapter do Implementation for `Dayron.Adapter.get/3`. """ def get(url, headers \\ [], opts \\ []) do - Client.get(url, headers: headers) |> translate_response + query = Keyword.get(opts, :params, []) + Client.get(url, headers: headers, query: query) |> translate_response end @doc """ diff --git a/test/lib/dayron/adapters/tesla_adapter_test.exs b/test/lib/dayron/adapters/tesla_adapter_test.exs index ba0f916..a9a5466 100644 --- a/test/lib/dayron/adapters/tesla_adapter_test.exs +++ b/test/lib/dayron/adapters/tesla_adapter_test.exs @@ -54,7 +54,6 @@ defmodule Dayron.TeslaAdapterTest do assert second[:name] == "Second Resource" end - @tag skip: "TBD" test "accepts query parameters and headers", %{bypass: bypass, api_url: api_url} do Bypass.expect bypass, fn conn -> assert "/resources" == conn.request_path From 967b4cedbe28aa6da23bf6094544bd32de4f2540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Tue, 11 Oct 2016 08:29:17 +0200 Subject: [PATCH 05/18] Make sure that Tesla supports custom headers --- test/lib/dayron/adapters/tesla_adapter_test.exs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/lib/dayron/adapters/tesla_adapter_test.exs b/test/lib/dayron/adapters/tesla_adapter_test.exs index a9a5466..c5d4af0 100644 --- a/test/lib/dayron/adapters/tesla_adapter_test.exs +++ b/test/lib/dayron/adapters/tesla_adapter_test.exs @@ -66,11 +66,10 @@ defmodule Dayron.TeslaAdapterTest do assert {:ok, %Dayron.Response{status_code: 200, body: _}} = response end - @tag skip: "TBD" test "accepts custom headers", %{bypass: bypass, api_url: api_url} do Bypass.expect bypass, fn conn -> assert "/resources/id" == conn.request_path - assert [_a, _b, {"accesstoken", "token"} | _] = conn.req_headers + assert {"accesstoken", "token"} in conn.req_headers assert "GET" == conn.method Plug.Conn.resp(conn, 200, "") end From 2e4d7864524c7d240ff0bc00cae2074b4b5f8e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Tue, 11 Oct 2016 08:30:44 +0200 Subject: [PATCH 06/18] Make sure that Tesla returns proper error response codes --- test/lib/dayron/adapters/tesla_adapter_test.exs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/lib/dayron/adapters/tesla_adapter_test.exs b/test/lib/dayron/adapters/tesla_adapter_test.exs index c5d4af0..5743dc3 100644 --- a/test/lib/dayron/adapters/tesla_adapter_test.exs +++ b/test/lib/dayron/adapters/tesla_adapter_test.exs @@ -77,7 +77,6 @@ defmodule Dayron.TeslaAdapterTest do assert {:ok, %Dayron.Response{status_code: 200, body: _}} = response end - @tag skip: "TBD" test "returns a 404 response", %{bypass: bypass, api_url: api_url} do Bypass.expect bypass, fn conn -> assert "/resources/invalid" == conn.request_path @@ -88,7 +87,6 @@ defmodule Dayron.TeslaAdapterTest do assert {:ok, %Dayron.Response{status_code: 404, body: _}} = response end - @tag skip: "TBD" test "returns a 500 error response", %{bypass: bypass, api_url: api_url} do Bypass.expect bypass, fn conn -> assert "/resources/server-error" == conn.request_path From 74143b884f71b89d2830314b6137f881b32c1c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Tue, 11 Oct 2016 08:39:11 +0200 Subject: [PATCH 07/18] Translate Tesla errors to properly handle invalid server, etc --- lib/dayron/adapters/tesla_adapter.ex | 9 ++++++++- test/lib/dayron/adapters/tesla_adapter_test.exs | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/dayron/adapters/tesla_adapter.ex b/lib/dayron/adapters/tesla_adapter.ex index 5dddc24..ca38635 100644 --- a/lib/dayron/adapters/tesla_adapter.ex +++ b/lib/dayron/adapters/tesla_adapter.ex @@ -31,7 +31,11 @@ defmodule Dayron.TeslaAdapter do """ def get(url, headers \\ [], opts \\ []) do query = Keyword.get(opts, :params, []) - Client.get(url, headers: headers, query: query) |> translate_response + try do + Client.get(url, headers: headers, query: query) |> translate_response + rescue + e in Tesla.Error -> translate_error(e) + end end @doc """ @@ -77,4 +81,7 @@ defmodule Dayron.TeslaAdapter do end end + defp translate_error(%Tesla.Error{reason: reason}) do + {:error, %Dayron.ClientError{reason: reason}} + end end diff --git a/test/lib/dayron/adapters/tesla_adapter_test.exs b/test/lib/dayron/adapters/tesla_adapter_test.exs index 5743dc3..14c464d 100644 --- a/test/lib/dayron/adapters/tesla_adapter_test.exs +++ b/test/lib/dayron/adapters/tesla_adapter_test.exs @@ -97,7 +97,6 @@ defmodule Dayron.TeslaAdapterTest do assert {:ok, %Dayron.Response{status_code: 500, body: _}} = response end - @tag skip: "TBD" test "returns an error for invalid server" do response = TeslaAdapter.get("http://localhost:0001/resources/error") assert {:error, %Dayron.ClientError{reason: :econnrefused}} = response From 25fdfada6214aa87c832df394740a2af20c103e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Tue, 11 Oct 2016 17:35:03 +0200 Subject: [PATCH 08/18] Encode to JSON with Tesla + use maps for headers --- lib/dayron/adapters/tesla_adapter.ex | 9 +++++---- test/lib/dayron/adapters/tesla_adapter_test.exs | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/dayron/adapters/tesla_adapter.ex b/lib/dayron/adapters/tesla_adapter.ex index ca38635..f832a2f 100644 --- a/lib/dayron/adapters/tesla_adapter.ex +++ b/lib/dayron/adapters/tesla_adapter.ex @@ -22,6 +22,7 @@ defmodule Dayron.TeslaAdapter do # returning errors along with raw output and perhaps allowing a force # flag to ignore content-type header contents, we could use that instead # of our own custom decoding. Perhaps a pull request would do it.. + plug Tesla.Middleware.EncodeJson, engine: Poison adapter Tesla.Adapter.Hackney end @@ -32,7 +33,7 @@ defmodule Dayron.TeslaAdapter do def get(url, headers \\ [], opts \\ []) do query = Keyword.get(opts, :params, []) try do - Client.get(url, headers: headers, query: query) |> translate_response + Client.get(url, headers: Enum.into(headers, %{}), query: query) |> translate_response rescue e in Tesla.Error -> translate_error(e) end @@ -42,21 +43,21 @@ defmodule Dayron.TeslaAdapter do Implementation for `Dayron.Adapter.post/4`. """ def post(url, body, headers \\ [], opts \\ []) do - Client.post(url, body, headers: headers) |> translate_response + Client.post(url, body, headers: Enum.into(headers, %{})) |> translate_response end @doc """ Implementation for `Dayron.Adapter.patch/4`. """ def patch(url, body, headers \\ [], opts \\ []) do - Client.patch(url, body, headers: headers) |> translate_response + Client.patch(url, body, headers: Enum.into(headers, %{})) |> translate_response end @doc """ Implementation for `Dayron.Adapter.delete/3`. """ def delete(url, headers \\ [], opts \\ []) do - Client.delete(url, headers: headers) |> translate_response + Client.delete(url, headers: Enum.into(headers, %{})) |> translate_response end defp translate_response(%Tesla.Env{} = response) do diff --git a/test/lib/dayron/adapters/tesla_adapter_test.exs b/test/lib/dayron/adapters/tesla_adapter_test.exs index 14c464d..b757d41 100644 --- a/test/lib/dayron/adapters/tesla_adapter_test.exs +++ b/test/lib/dayron/adapters/tesla_adapter_test.exs @@ -102,7 +102,6 @@ defmodule Dayron.TeslaAdapterTest do assert {:error, %Dayron.ClientError{reason: :econnrefused}} = response end - @tag skip: "TBD" test "returns a decoded body for a valid post request", %{bypass: bypass, api_url: api_url} do Bypass.expect bypass, fn conn -> assert "/resources" == conn.request_path From 4a4e44204b79a3f87bba37acd1c0024f1d0ce6b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Tue, 11 Oct 2016 17:51:32 +0200 Subject: [PATCH 09/18] Make sure patch requests are properly handled --- test/lib/dayron/adapters/tesla_adapter_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/lib/dayron/adapters/tesla_adapter_test.exs b/test/lib/dayron/adapters/tesla_adapter_test.exs index b757d41..3f91362 100644 --- a/test/lib/dayron/adapters/tesla_adapter_test.exs +++ b/test/lib/dayron/adapters/tesla_adapter_test.exs @@ -115,7 +115,6 @@ defmodule Dayron.TeslaAdapterTest do assert body[:age] == 30 end - @tag skip: "TBD" test "returns a decoded body for a valid patch request", %{bypass: bypass, api_url: api_url} do Bypass.expect bypass, fn conn -> assert "/resources/id" == conn.request_path From b69bf5bb1811c662a48daf27651fae125c9b1541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Tue, 11 Oct 2016 17:55:09 +0200 Subject: [PATCH 10/18] Convert empty response body to nil --- lib/dayron/adapters/tesla_adapter.ex | 1 + test/lib/dayron/adapters/tesla_adapter_test.exs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dayron/adapters/tesla_adapter.ex b/lib/dayron/adapters/tesla_adapter.ex index f832a2f..ba6fc0e 100644 --- a/lib/dayron/adapters/tesla_adapter.ex +++ b/lib/dayron/adapters/tesla_adapter.ex @@ -73,6 +73,7 @@ defmodule Dayron.TeslaAdapter do {:error, struct(Dayron.ClientError, data)} end + defp translate_response_body(""), do: nil defp translate_response_body("ok"), do: %{} defp translate_response_body(body) do try do diff --git a/test/lib/dayron/adapters/tesla_adapter_test.exs b/test/lib/dayron/adapters/tesla_adapter_test.exs index 3f91362..15a3e5e 100644 --- a/test/lib/dayron/adapters/tesla_adapter_test.exs +++ b/test/lib/dayron/adapters/tesla_adapter_test.exs @@ -128,7 +128,6 @@ defmodule Dayron.TeslaAdapterTest do assert body[:age] == 30 end - @tag skip: "TBD" test "returns an empty body for a valid delete request", %{bypass: bypass, api_url: api_url} do Bypass.expect bypass, fn conn -> assert "/resources/id" == conn.request_path From aff50ee1028bce558ce1f3a5737cac05b824a7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Tue, 11 Oct 2016 18:20:07 +0200 Subject: [PATCH 11/18] Use Tesla middleware for response translation --- lib/dayron/adapters/tesla_adapter.ex | 68 +++++++++++++++++----------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/lib/dayron/adapters/tesla_adapter.ex b/lib/dayron/adapters/tesla_adapter.ex index ba6fc0e..d3fa001 100644 --- a/lib/dayron/adapters/tesla_adapter.ex +++ b/lib/dayron/adapters/tesla_adapter.ex @@ -23,17 +23,54 @@ defmodule Dayron.TeslaAdapter do # flag to ignore content-type header contents, we could use that instead # of our own custom decoding. Perhaps a pull request would do it.. plug Tesla.Middleware.EncodeJson, engine: Poison + plug Dayron.TeslaAdapter.Translator adapter Tesla.Adapter.Hackney end + defmodule Translator do + @moduledoc """ + A Tesla Middleware implementation, translating responses to the format + expected by Dayron. + """ + def call(env, next, opts \\ []) do + env + |> Tesla.run(next) + |> translate_response() + end + + defp translate_response(%Tesla.Env{} = response) do + {:ok, %Dayron.Response{ + status_code: response.status, + body: translate_response_body(response.body), + headers: response.headers |> Map.to_list + } + } + end + defp translate_response({:error, response}) do + data = response |> Map.from_struct + {:error, struct(Dayron.ClientError, data)} + end + + defp translate_response_body(""), do: nil + defp translate_response_body("ok"), do: %{} + defp translate_response_body(body) do + try do + body |> Poison.decode!(keys: :atoms) + rescue + Poison.SyntaxError -> body + end + end + end + @doc """ Implementation for `Dayron.Adapter.get/3`. """ def get(url, headers \\ [], opts \\ []) do query = Keyword.get(opts, :params, []) + # TODO: Implement try..catch for remaining functions? try do - Client.get(url, headers: Enum.into(headers, %{}), query: query) |> translate_response + Client.get(url, headers: Enum.into(headers, %{}), query: query) rescue e in Tesla.Error -> translate_error(e) end @@ -43,44 +80,21 @@ defmodule Dayron.TeslaAdapter do Implementation for `Dayron.Adapter.post/4`. """ def post(url, body, headers \\ [], opts \\ []) do - Client.post(url, body, headers: Enum.into(headers, %{})) |> translate_response + Client.post(url, body, headers: Enum.into(headers, %{})) end @doc """ Implementation for `Dayron.Adapter.patch/4`. """ def patch(url, body, headers \\ [], opts \\ []) do - Client.patch(url, body, headers: Enum.into(headers, %{})) |> translate_response + Client.patch(url, body, headers: Enum.into(headers, %{})) end @doc """ Implementation for `Dayron.Adapter.delete/3`. """ def delete(url, headers \\ [], opts \\ []) do - Client.delete(url, headers: Enum.into(headers, %{})) |> translate_response - end - - defp translate_response(%Tesla.Env{} = response) do - {:ok, %Dayron.Response{ - status_code: response.status, - body: translate_response_body(response.body), - headers: response.headers |> Map.to_list - } - } - end - defp translate_response({:error, response}) do - data = response |> Map.from_struct - {:error, struct(Dayron.ClientError, data)} - end - - defp translate_response_body(""), do: nil - defp translate_response_body("ok"), do: %{} - defp translate_response_body(body) do - try do - body |> Poison.decode!(keys: :atoms) - rescue - Poison.SyntaxError -> body - end + Client.delete(url, headers: Enum.into(headers, %{})) end defp translate_error(%Tesla.Error{reason: reason}) do From b542bbe0a5c908de822a866df917b2870f172afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Wed, 12 Oct 2016 08:52:29 +0200 Subject: [PATCH 12/18] Unify error translation and wrap all Tesla calls --- lib/dayron/adapters/tesla_adapter.ex | 29 +++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/dayron/adapters/tesla_adapter.ex b/lib/dayron/adapters/tesla_adapter.ex index d3fa001..07686de 100644 --- a/lib/dayron/adapters/tesla_adapter.ex +++ b/lib/dayron/adapters/tesla_adapter.ex @@ -48,8 +48,7 @@ defmodule Dayron.TeslaAdapter do } end defp translate_response({:error, response}) do - data = response |> Map.from_struct - {:error, struct(Dayron.ClientError, data)} + translate_error(response) end defp translate_response_body(""), do: nil @@ -61,6 +60,11 @@ defmodule Dayron.TeslaAdapter do Poison.SyntaxError -> body end end + + def translate_error(%Tesla.Error{} = error) do + data = error |> Map.from_struct + {:error, struct(Dayron.ClientError, data)} + end end @doc """ @@ -68,36 +72,35 @@ defmodule Dayron.TeslaAdapter do """ def get(url, headers \\ [], opts \\ []) do query = Keyword.get(opts, :params, []) - # TODO: Implement try..catch for remaining functions? - try do - Client.get(url, headers: Enum.into(headers, %{}), query: query) - rescue - e in Tesla.Error -> translate_error(e) - end + tesla_call(:get, [url, [headers: Enum.into(headers, %{}), query: query]]) end @doc """ Implementation for `Dayron.Adapter.post/4`. """ def post(url, body, headers \\ [], opts \\ []) do - Client.post(url, body, headers: Enum.into(headers, %{})) + tesla_call(:post, [url, body, [headers: Enum.into(headers, %{})]]) end @doc """ Implementation for `Dayron.Adapter.patch/4`. """ def patch(url, body, headers \\ [], opts \\ []) do - Client.patch(url, body, headers: Enum.into(headers, %{})) + tesla_call(:patch, [url, body, [headers: Enum.into(headers, %{})]]) end @doc """ Implementation for `Dayron.Adapter.delete/3`. """ def delete(url, headers \\ [], opts \\ []) do - Client.delete(url, headers: Enum.into(headers, %{})) + tesla_call(:delete, [url, [headers: Enum.into(headers, %{})]]) end - defp translate_error(%Tesla.Error{reason: reason}) do - {:error, %Dayron.ClientError{reason: reason}} + defp tesla_call(method, args) do + try do + apply(Client, method, args) + rescue + e in Tesla.Error -> Translator.translate_error(e) + end end end From bd051db9090fa197688be50dd2e0ff1a1818746c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Wed, 12 Oct 2016 15:50:30 +0200 Subject: [PATCH 13/18] Use Elixir 1.3 in CI, required for Tesla --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 10040d4..ef44c1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: elixir elixir: - - 1.2.3 + - 1.3.4 otp_release: - 18.2.1 env: From a9031815914ea2b72eb688430ff2d975f218e42f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Wed, 12 Oct 2016 16:10:28 +0200 Subject: [PATCH 14/18] Update Tesla adapter docs + add remaining TODO items --- lib/dayron/adapters/tesla_adapter.ex | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/dayron/adapters/tesla_adapter.ex b/lib/dayron/adapters/tesla_adapter.ex index 07686de..3356bb9 100644 --- a/lib/dayron/adapters/tesla_adapter.ex +++ b/lib/dayron/adapters/tesla_adapter.ex @@ -7,6 +7,11 @@ defmodule Dayron.TeslaAdapter do config :my_app, MyApp.Repo, adapter: Dayron.TeslaAdapter, url: "https://api.example.com" + + ## TODO + + - Handle options to the Tesla client, see `Dayron.Adapter` + - Check test coverage """ @behaviour Dayron.Adapter @@ -18,10 +23,6 @@ defmodule Dayron.TeslaAdapter do """ use Tesla - # TODO: If the Tesla JSON decoding was a bit more flexible, properly - # returning errors along with raw output and perhaps allowing a force - # flag to ignore content-type header contents, we could use that instead - # of our own custom decoding. Perhaps a pull request would do it.. plug Tesla.Middleware.EncodeJson, engine: Poison plug Dayron.TeslaAdapter.Translator @@ -32,6 +33,12 @@ defmodule Dayron.TeslaAdapter do @moduledoc """ A Tesla Middleware implementation, translating responses to the format expected by Dayron. + + We're also doing JSON decoding of responses here, as the built-in JSON + middleware in Tesla will only decode content-type `application/json`, as + well as raise an error on decoding issues instead of returning the raw + input. Both of these differences break the existing implicit contract, as + implemented by `Dayron.HTTPoisonAdapter`. """ def call(env, next, opts \\ []) do env From ec400413b9e7146cb472cd37727df38d237f707b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Thu, 13 Oct 2016 07:00:35 +0200 Subject: [PATCH 15/18] Tesla always raises on error; don't handle {:error, _} results --- lib/dayron/adapters/tesla_adapter.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/dayron/adapters/tesla_adapter.ex b/lib/dayron/adapters/tesla_adapter.ex index 3356bb9..6fb84c7 100644 --- a/lib/dayron/adapters/tesla_adapter.ex +++ b/lib/dayron/adapters/tesla_adapter.ex @@ -11,7 +11,6 @@ defmodule Dayron.TeslaAdapter do ## TODO - Handle options to the Tesla client, see `Dayron.Adapter` - - Check test coverage """ @behaviour Dayron.Adapter @@ -54,9 +53,6 @@ defmodule Dayron.TeslaAdapter do } } end - defp translate_response({:error, response}) do - translate_error(response) - end defp translate_response_body(""), do: nil defp translate_response_body("ok"), do: %{} From 30429546b1aa691d4c1b4190e665c3d1d4fdd63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Fri, 14 Oct 2016 07:28:16 +0200 Subject: [PATCH 16/18] Handle Hackney options for Tesla --- lib/dayron/adapters/tesla_adapter.ex | 41 +++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/lib/dayron/adapters/tesla_adapter.ex b/lib/dayron/adapters/tesla_adapter.ex index 6fb84c7..6009da8 100644 --- a/lib/dayron/adapters/tesla_adapter.ex +++ b/lib/dayron/adapters/tesla_adapter.ex @@ -10,7 +10,7 @@ defmodule Dayron.TeslaAdapter do ## TODO - - Handle options to the Tesla client, see `Dayron.Adapter` + - Handle streaming """ @behaviour Dayron.Adapter @@ -39,7 +39,7 @@ defmodule Dayron.TeslaAdapter do input. Both of these differences break the existing implicit contract, as implemented by `Dayron.HTTPoisonAdapter`. """ - def call(env, next, opts \\ []) do + def call(env, next, _opts) do env |> Tesla.run(next) |> translate_response() @@ -74,29 +74,28 @@ defmodule Dayron.TeslaAdapter do Implementation for `Dayron.Adapter.get/3`. """ def get(url, headers \\ [], opts \\ []) do - query = Keyword.get(opts, :params, []) - tesla_call(:get, [url, [headers: Enum.into(headers, %{}), query: query]]) + tesla_call(:get, [url, build_options(headers, opts)]) end @doc """ Implementation for `Dayron.Adapter.post/4`. """ def post(url, body, headers \\ [], opts \\ []) do - tesla_call(:post, [url, body, [headers: Enum.into(headers, %{})]]) + tesla_call(:post, [url, body, build_options(headers, opts)]) end @doc """ Implementation for `Dayron.Adapter.patch/4`. """ def patch(url, body, headers \\ [], opts \\ []) do - tesla_call(:patch, [url, body, [headers: Enum.into(headers, %{})]]) + tesla_call(:patch, [url, body, build_options(headers, opts)]) end @doc """ Implementation for `Dayron.Adapter.delete/3`. """ def delete(url, headers \\ [], opts \\ []) do - tesla_call(:delete, [url, [headers: Enum.into(headers, %{})]]) + tesla_call(:delete, [url, build_options(headers, opts)]) end defp tesla_call(method, args) do @@ -106,4 +105,32 @@ defmodule Dayron.TeslaAdapter do e in Tesla.Error -> Translator.translate_error(e) end end + + def build_options([], opts), do: build_options(opts) + def build_options(headers, opts) do + build_options([{:headers, Enum.into(headers, %{})} | opts]) + end + + defp build_options(opts) do + Enum.reduce(opts, [{:opts, build_hackney_options(opts)}], fn + {:headers, value}, options -> [{:headers, value} | options] + {:params, value}, options -> [{:query, value} | options] + end) + end + + defp build_hackney_options(opts) do + Enum.reduce(opts, [], fn + {:hackney, extra_opts}, hn_opts -> hn_opts ++ extra_opts + {:timeout, value}, hn_opts -> [{:connect_timeout, value} | hn_opts] + {:recv_timeout, value}, hn_opts -> [{:recv_timeout, value} | hn_opts] + {:proxy, value}, hn_opts -> [{:proxy, value} | hn_opts] + {:proxy_auth, value}, hn_opts -> [{:proxy_auth, value} | hn_opts] + {:ssl, value}, hn_opts -> [{:ssl_options, value} | hn_opts] + {:follow_redirect, value}, hn_opts -> [{:follow_redirect, value} | hn_opts] + {:max_redirect, value}, hn_opts -> [{:max_redirect, value} | hn_opts] + #{:stream_to, arg}, hn_opts -> + # [:async, {:stream_to, spawn(module, :transformer, [arg])} | hn_opts] + _other, hn_opts -> hn_opts + end) + end end From 10b0c5010a54daf2c179c2137ac46d3beae1be5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Tue, 25 Oct 2016 10:37:53 +0200 Subject: [PATCH 17/18] Add test for passing a custom hackney option --- lib/dayron/adapters/tesla_adapter.ex | 1 + .../dayron/adapters/tesla_adapter_test.exs | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/lib/dayron/adapters/tesla_adapter.ex b/lib/dayron/adapters/tesla_adapter.ex index 6009da8..3e5d348 100644 --- a/lib/dayron/adapters/tesla_adapter.ex +++ b/lib/dayron/adapters/tesla_adapter.ex @@ -115,6 +115,7 @@ defmodule Dayron.TeslaAdapter do Enum.reduce(opts, [{:opts, build_hackney_options(opts)}], fn {:headers, value}, options -> [{:headers, value} | options] {:params, value}, options -> [{:query, value} | options] + _, options -> options end) end diff --git a/test/lib/dayron/adapters/tesla_adapter_test.exs b/test/lib/dayron/adapters/tesla_adapter_test.exs index 15a3e5e..66c7313 100644 --- a/test/lib/dayron/adapters/tesla_adapter_test.exs +++ b/test/lib/dayron/adapters/tesla_adapter_test.exs @@ -138,4 +138,24 @@ defmodule Dayron.TeslaAdapterTest do response = TeslaAdapter.delete("#{api_url}/resources/id", [{"content-type", "application/json"}]) assert {:ok, %Dayron.Response{status_code: 204, body: nil}} = response end + + test "passing a custom hackney option works", %{bypass: bypass, api_url: api_url} do + Bypass.expect bypass, fn conn -> + case conn.request_path do + "/old" -> + conn + |> Plug.Conn.put_resp_header("location", "/new") + |> Plug.Conn.resp(301, "You are being redirected.") + |> Plug.Conn.halt + "/new" -> + Plug.Conn.resp(conn, 200, "bar") + end + end + response = TeslaAdapter.post("#{api_url}/old", "foo", [], [ + {:follow_redirect, true}, + {:hackney, [{:force_redirect, true}]} + ]) + assert {:ok, %Dayron.Response{status_code: 200, body: body}} = response + assert body == "bar" + end end From 5b68a565e575ac948e3778643f07a3945ff9570d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20W=C3=A4rlander?= Date: Tue, 25 Oct 2016 11:58:23 +0200 Subject: [PATCH 18/18] Add information about Tesla adapter to the README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f6153d6..a44dd88 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,11 @@ Using the configuration you're allowed to set headers that will be sent on every ### HTTP Client Adapter -Currently the only adapter available is [HTTPoisonAdapter](https://github.com/inaka/Dayron/blob/master/lib/dayron/adapters/httpoison_adapter.ex), which uses [HTTPoison](https://github.com/edgurgel/httpoison) and [hackney](https://github.com/benoitc/hackney) to manage HTTP requests. +Currently the adapters available are: +- [HTTPoisonAdapter](https://github.com/inaka/Dayron/blob/master/lib/dayron/adapters/httpoison_adapter.ex), which uses [HTTPoison](https://github.com/edgurgel/httpoison) and [hackney](https://github.com/benoitc/hackney) to manage HTTP requests +- [TeslaAdapter](https://github.com/inaka/Dayron/blob/master/lib/dayron/adapters/tesla_adapter.ex), which uses [Tesla](https://github.com/teamon/tesla) and [hackney](https://github.com/benoitc/hackney) to manage HTTP requests + +**NOTE:** While the HTTPoison adapter accepts the `:stream_to` argument and passes it on to HTTPoison, streaming isn't very well supported yet as it's not handled by the adapter in a generic way. The Tesla adapter currently ignores the option. See [discussion in issue #54](https://github.com/inaka/Dayron/issues/54#issuecomment-253715077). You can also create your own adapter implementing the [Dyron.Adapter](https://github.com/inaka/Dayron/blob/master/lib/dayron/adapter.ex) behavior, and changing the configuration to something like: