diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs new file mode 100644 index 0000000..e26e2da --- /dev/null +++ b/.dialyzer_ignore.exs @@ -0,0 +1,3 @@ +[ + ~r/__impl__.*does\ not\ exist\./ +] diff --git a/lib/stow/adapter/httpc.ex b/lib/stow/adapter/httpc.ex index 7af1248..f0e48a3 100644 --- a/lib/stow/adapter/httpc.ex +++ b/lib/stow/adapter/httpc.ex @@ -3,6 +3,8 @@ defmodule Stow.Adapter.Httpc do @behaviour Stow.Adapter import Stow.URI, only: [to_iolist: 1] + import Stow.ResponseHandler + alias Stow.Conn @http_options [ @@ -19,21 +21,12 @@ defmodule Stow.Adapter.Httpc do def dispatch(%Conn{method: :get} = conn) do with {http_opts, opts} <- split_options(conn.opts, {[], []}, conn.uri.scheme), headers <- maybe_to_charlist(conn.headers) do - :httpc.request(:get, {to_iolist(conn.uri), headers}, http_opts, opts) - |> handle_response() + :httpc.request(:get, {to_iolist(conn.uri), headers}, http_opts, opts) |> to_response() end end def dispatch(_conn), do: {:error, :not_supported} - defp handle_response({:ok, {{[?H, ?T, ?T, ?P | _], status, _}, headers, body}}) do - {:ok, - {status, Enum.map(headers, fn {k, v} -> {to_string(k), to_string(v)} end), - body |> IO.iodata_to_binary()}} - end - - defp handle_response({:error, reason}), do: {:error, reason} - defp split_options(nil, {http_opts, opts}, _), do: {http_opts, opts} defp split_options([], {http_opts, opts}, "http"), do: {http_opts, opts} defp split_options([], {http_opts, opts}, "https"), do: {http_opts |> set_ssl_opt(), opts} diff --git a/lib/stow/response.ex b/lib/stow/response.ex new file mode 100644 index 0000000..b5b52a3 --- /dev/null +++ b/lib/stow/response.ex @@ -0,0 +1,35 @@ +defmodule Stow.Response do + @moduledoc """ + Struct containing a standardised response from various data sources. + """ + + defstruct [:body, :headers, :state, :status] + + @type t :: %__MODULE__{ + body: nil | iodata(), + headers: nil | [{binary(), binary()}], + status: nil | pos_integer(), + state: nil | :ok | {:error, term()} + } + + def put_body(response, body) when is_binary(body) do + %{response | body: body} + end + + def put_body(response, body) when is_list(body) do + %{response | body: if(IO.iodata_length(body) > 0, do: body, else: nil)} + end + + def put_headers(response, [{k, v} | _] = headers) when is_binary(k) and is_binary(v) do + %{response | headers: headers} + end + + def put_headers(response, [{k, v} | _] = headers) + when is_list(k) and is_list(v) and is_integer(hd(k)) and is_integer(hd(v)) do + %{response | headers: headers |> Enum.map(fn {k, v} -> {to_string(k), to_string(v)} end)} + end + + def put_status(response, status) when is_integer(status) do + %{response | status: status} + end +end diff --git a/lib/stow/response_handler.ex b/lib/stow/response_handler.ex new file mode 100644 index 0000000..00b1ec1 --- /dev/null +++ b/lib/stow/response_handler.ex @@ -0,0 +1,34 @@ +defprotocol Stow.ResponseHandler do + @moduledoc ~S""" + The `Stow.Response` protocol is responsible for + converting various data sources response to the `Stow.Response.t()` struct. + + The only function that must be implemented is + `to_response/1` which does the conversion. + """ + + @dialyzer {:nowarn_function, to_response: 1} + + @fallback_to_any true + @spec to_response(term()) :: Stow.Response.t() + def to_response(value) +end + +defimpl Stow.ResponseHandler, for: Tuple do + alias Stow.Response + import Stow.Response + + # httpc response + def to_response({:ok, {{[?H, ?T, ?T, ?P | _], status, _}, headers, body}}) do + %Response{state: :ok} + |> put_body(body |> IO.iodata_to_binary()) + |> put_status(status) + |> put_headers(headers) + end + + def to_response({:error, term}), do: %Stow.Response{state: {:error, term}} +end + +defimpl Stow.ResponseHandler, for: Any do + def to_response(_value), do: %Stow.Response{} +end diff --git a/lib/stow/source.ex b/lib/stow/source.ex index 904447c..a22e16f 100644 --- a/lib/stow/source.ex +++ b/lib/stow/source.ex @@ -16,7 +16,7 @@ defmodule Stow.Source do @type conn :: Plug.Conn.t() - @callback get(conn(), map()) :: HttpClient.response() + @callback get(conn(), map()) :: term() def new(uri, options \\ nil), do: %__MODULE__{uri: uri, options: options} end diff --git a/test/stow/adapter/httpc_test.exs b/test/stow/adapter/httpc_test.exs index 3d2421e..0ad9fbe 100644 --- a/test/stow/adapter/httpc_test.exs +++ b/test/stow/adapter/httpc_test.exs @@ -2,8 +2,9 @@ defmodule Stow.Adapter.HttpcTest do use ExUnit.Case, async: true import ExUnit.CaptureLog - alias Stow.Conn alias Stow.Adapter.Httpc + alias Stow.Conn + alias Stow.Response setup do bypass = Bypass.open() @@ -26,8 +27,16 @@ defmodule Stow.Adapter.HttpcTest do } end - describe "dispatch/1 for uri" do - test "with request path and query string", %{bypass: bypass, conn: conn} do + defp assert_ok_response(response) do + assert %Response{} = response + assert {"server", "Cowboy"} in response.headers + assert response.status == 200 + assert response.body == "getting a response" + assert response.state == :ok + end + + describe "dispatch/1" do + test "request path and query string", %{bypass: bypass, conn: conn} do Bypass.expect(bypass, fn conn -> request_url = Plug.Conn.request_url(conn) assert request_url == "http://localhost:#{bypass.port}/request/path?foo=bar" @@ -36,10 +45,7 @@ defmodule Stow.Adapter.HttpcTest do Plug.Conn.resp(conn, 200, "getting a response") end) - assert {:ok, {status, headers, body}} = conn |> Httpc.dispatch() - assert status == 200 - assert {"server", "Cowboy"} in headers - assert body == "getting a response" + conn |> Httpc.dispatch() |> assert_ok_response() end test "without request path and query string ", %{bypass: bypass, conn: conn, uri: uri} do @@ -52,10 +58,7 @@ defmodule Stow.Adapter.HttpcTest do end) uri = %{uri | query: "", path: ""} - conn = %{conn | uri: uri} - assert {:ok, {status, _headers, body}} = conn |> Httpc.dispatch() - assert status == 200 - assert body == "getting a response" + %{conn | uri: uri} |> Httpc.dispatch() |> assert_ok_response() end test "without query string ", %{bypass: bypass, conn: conn, uri: uri} do @@ -68,10 +71,7 @@ defmodule Stow.Adapter.HttpcTest do end) uri = %{uri | query: ""} - conn = %{conn | uri: uri} - assert {:ok, {status, _headers, body}} = conn |> Httpc.dispatch() - assert status == 200 - assert body == "getting a response" + %{conn | uri: uri} |> Httpc.dispatch() |> assert_ok_response() end test "in https scheme", %{conn: conn, opts: opts, uri: uri} do @@ -89,7 +89,7 @@ defmodule Stow.Adapter.HttpcTest do conn = %{conn | opts: opts} Bypass.expect(bypass, fn conn -> Plug.Conn.resp(conn, 200, "getting a response") end) - assert {:ok, _} = conn |> Httpc.dispatch() + conn |> Httpc.dispatch() |> assert_ok_response() end test "with charlist (native) request headers", %{bypass: bypass, conn: conn} do @@ -98,8 +98,6 @@ defmodule Stow.Adapter.HttpcTest do {~c"accept-Language", ~c"en-US,en;q=0.5"} ] - conn = %{conn | headers: req_headers} - Bypass.expect(bypass, fn conn -> assert {"accept", "application/json,text/html"} in conn.req_headers assert {"accept-language", "en-US,en;q=0.5"} in conn.req_headers @@ -107,7 +105,7 @@ defmodule Stow.Adapter.HttpcTest do Plug.Conn.resp(conn, 200, "getting a response") end) - assert {:ok, _} = conn |> Httpc.dispatch() + %{conn | headers: req_headers} |> Httpc.dispatch() |> assert_ok_response() end test "with binary request headers", %{bypass: bypass, conn: conn} do @@ -116,8 +114,6 @@ defmodule Stow.Adapter.HttpcTest do {"accept-Language", "en-US,en;q=0.5"} ] - conn = %{conn | headers: req_headers} - Bypass.expect(bypass, fn conn -> assert {"accept", "application/json,text/html"} in conn.req_headers assert {"accept-language", "en-US,en;q=0.5"} in conn.req_headers @@ -125,7 +121,7 @@ defmodule Stow.Adapter.HttpcTest do Plug.Conn.resp(conn, 200, "getting a response") end) - assert {:ok, _} = conn |> Httpc.dispatch() + %{conn | headers: req_headers} |> Httpc.dispatch() |> assert_ok_response() end test "returns error on invalid headers", %{conn: conn} do @@ -134,8 +130,8 @@ defmodule Stow.Adapter.HttpcTest do {"accept-Language", "en-US,en;q=0.5"} ] - conn = %{conn | headers: req_headers} - assert {:error, {:headers_error, :invalid_field}} = conn |> Httpc.dispatch() + assert %Response{} = response = %{conn | headers: req_headers} |> Httpc.dispatch() + assert response.state == {:error, {:headers_error, :invalid_field}} end end