Skip to content

Commit

Permalink
introduce Response and ResponseHandler protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
boonious committed Jul 13, 2024
1 parent 2ff00a0 commit 3db4182
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 35 deletions.
3 changes: 3 additions & 0 deletions .dialyzer_ignore.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
~r/__impl__.*does\ not\ exist\./
]
13 changes: 3 additions & 10 deletions lib/stow/adapter/httpc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand All @@ -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}
Expand Down
35 changes: 35 additions & 0 deletions lib/stow/response.ex
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions lib/stow/response_handler.ex
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion lib/stow/source.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 20 additions & 24 deletions test/stow/adapter/httpc_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -98,16 +98,14 @@ 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

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
Expand All @@ -116,16 +114,14 @@ 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

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
Expand All @@ -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

Expand Down

0 comments on commit 3db4182

Please sign in to comment.