Skip to content

Commit

Permalink
Merge pull request #35 from inaka/flavio.32.add-a-dayron-response
Browse files Browse the repository at this point in the history
[#32] add a dayron response, adapter returning structs in dayron namespace
  • Loading branch information
alemata committed May 13, 2016
2 parents 08afe38 + e66a829 commit 87b7451
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 83 deletions.
7 changes: 3 additions & 4 deletions lib/dayron/adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ defmodule Dayron.Adapter do
end
end
"""
require HTTPoison
alias HTTPoison.Response
alias HTTPoison.Error
alias Dayron.Response
alias Dayron.ClientError

@type headers :: [{binary, binary}] | %{binary => binary}
@type body :: struct
@type response :: {:ok, Response.t} | {:error, Error.t}
@type response :: {:ok, Response.t} | {:error, ClientError.t}

@doc """
Issues a GET request to the given url. The headers param is an enumerable
Expand Down
18 changes: 14 additions & 4 deletions lib/dayron/adapters/httpoison_adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Dayron.HTTPoisonAdapter do
url: "https://api.example.com"
"""
@behaviour Dayron.Adapter
require HTTPoison

defmodule Client do
@moduledoc """
Expand Down Expand Up @@ -55,30 +56,39 @@ defmodule Dayron.HTTPoisonAdapter do
"""
def get(url, headers \\ [], opts \\ []) do
Client.start
Client.get(url, headers, opts)
Client.get(url, headers, opts) |> translate_response
end

@doc """
Implementation for `Dayron.Adapter.post/4`.
"""
def post(url, body, headers \\ [], opts \\ []) do
Client.start
Client.post(url, body, headers, opts)
Client.post(url, body, headers, opts) |> translate_response
end

@doc """
Implementation for `Dayron.Adapter.patch/4`.
"""
def patch(url, body, headers \\ [], opts \\ []) do
Client.start
Client.patch(url, body, headers, opts)
Client.patch(url, body, headers, opts) |> translate_response
end

@doc """
Implementation for `Dayron.Adapter.delete/3`.
"""
def delete(url, headers \\ [], opts \\ []) do
Client.start
Client.delete(url, headers, opts)
Client.delete(url, headers, opts) |> translate_response
end

defp translate_response({:ok, response}) do
data = response |> Map.from_struct
{:ok, struct(Dayron.Response, data)}
end
defp translate_response({:error, response}) do
data = response |> Map.from_struct
{:error, struct(Dayron.ClientError, data)}
end
end
2 changes: 1 addition & 1 deletion lib/dayron/exceptions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ defmodule Dayron.ClientError do
@moduledoc """
Raised at runtime when the request connection fails.
"""
defexception [:url, :method, :reason]
defexception [:id, :url, :method, :reason]

def message(%{url: url, method: method, reason: reason}) do
"""
Expand Down
9 changes: 5 additions & 4 deletions lib/dayron/logger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ defmodule Dayron.Logger do
@moduledoc """
Helper module wrapping Logger calls to register request/response events
"""
require HTTPoison
require Logger
alias Dayron.Response
alias Dayron.ClientError

@doc """
Logs a debug or error message based on response code.
Expand All @@ -16,23 +17,23 @@ defmodule Dayron.Logger do
@doc """
Logs a debug message for response codes between 200-399.
"""
def do_log(method, url, %HTTPoison.Response{status_code: code}, req_details) when code < 400 do
def do_log(method, url, %Response{status_code: code}, req_details) when code < 400 do
Logger.debug [method, ?\s, url, ?\s, "-> #{code}"]
log_request_details :debug, req_details
end

@doc """
Logs an error message for error response codes, or greater than 400.
"""
def do_log(method, url, %HTTPoison.Response{status_code: code}, req_details) do
def do_log(method, url, %Response{status_code: code}, req_details) do
Logger.error [method, ?\s, url, ?\s, "-> #{code}"]
log_request_details :debug, req_details
end

@doc """
Logs an error message for response error/exception.
"""
def do_log(method, url, %HTTPoison.Error{reason: reason}, req_details) do
def do_log(method, url, %ClientError{reason: reason}, req_details) do
Logger.error [method, ?\s, url, ?\s, "-> #{reason}"]
log_request_details :error, req_details
end
Expand Down
70 changes: 38 additions & 32 deletions lib/dayron/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -231,54 +231,58 @@ defmodule Dayron.Repo do

@doc false
def get(adapter, model, id, opts, config) do
{_request, response} = config
|> Config.init_request_data(:get, model, id: id)
|> execute!(adapter, opts, config)
{_request, response} =
config
|> Config.init_request_data(:get, model, id: id)
|> execute!(adapter, opts, config)

case response do
%HTTPoison.Response{status_code: 200, body: body} ->
%Dayron.Response{status_code: 200, body: body} ->
Model.from_json(model, body)
%HTTPoison.Response{status_code: code} when code >= 300 and code < 500 ->
%Dayron.Response{status_code: code} when code >= 300 and code < 500 ->
nil
end
end

@doc false
def get!(adapter, model, id, opts, config) do
{request, response} = config
|> Config.init_request_data(:get, model, id: id)
|> execute!(adapter, opts, config)
{request, response} =
config
|> Config.init_request_data(:get, model, id: id)
|> execute!(adapter, opts, config)

case response do
%HTTPoison.Response{status_code: 200, body: body} ->
%Dayron.Response{status_code: 200, body: body} ->
Model.from_json(model, body)
%HTTPoison.Response{status_code: code} when code >= 300 and code < 500 ->
%Dayron.Response{status_code: code} when code >= 300 and code < 500 ->
raise Dayron.NoResultsError, method: request.method, url: request.url
end
end

@doc false
def all(adapter, model, opts, config) do
{_request, response} = config
|> Config.init_request_data(:get, model)
|> execute!(adapter, opts, config)
{_request, response} =
config
|> Config.init_request_data(:get, model)
|> execute!(adapter, opts, config)

case response do
%HTTPoison.Response{status_code: 200, body: body} ->
%Dayron.Response{status_code: 200, body: body} ->
Model.from_json_list(model, body)
end
end

@doc false
def insert(adapter, model, data, opts, config) do
{request, response} = config
|> Config.init_request_data(:post, model, body: data)
|> execute!(adapter, opts, config)
{request, response} =
config
|> Config.init_request_data(:post, model, body: data)
|> execute!(adapter, opts, config)

case response do
%HTTPoison.Response{status_code: 201, body: body} ->
%Dayron.Response{status_code: 201, body: body} ->
{:ok, Model.from_json(model, body)}
%HTTPoison.Response{status_code: 422, body: body} ->
%Dayron.Response{status_code: 422, body: body} ->
{:error, %{method: request.method, url: request.url, response: body}}
end
end
Expand All @@ -293,14 +297,15 @@ defmodule Dayron.Repo do

@doc false
def update(adapter, model, data, opts, config) do
{request, response} = config
|> Config.init_request_data(:patch, model, data)
|> execute!(adapter, opts, config)
{request, response} =
config
|> Config.init_request_data(:patch, model, data)
|> execute!(adapter, opts, config)

case response do
%HTTPoison.Response{status_code: 200, body: body} ->
%Dayron.Response{status_code: 200, body: body} ->
{:ok, Model.from_json(model, body)}
%HTTPoison.Response{status_code: code, body: body}
%Dayron.Response{status_code: code, body: body}
when code >= 400 and code < 500 ->
{:error, %{method: request.method, code: code, url: request.url,
response: body}}
Expand All @@ -320,15 +325,16 @@ defmodule Dayron.Repo do

@doc false
def delete(adapter, model, id, opts, config) do
{request, response} = config
|> Config.init_request_data(:delete, model, id: id)
|> execute!(adapter, opts, config)
{request, response} =
config
|> Config.init_request_data(:delete, model, id: id)
|> execute!(adapter, opts, config)

case response do
%HTTPoison.Response{status_code: 200, body: body} ->
%Dayron.Response{status_code: 200, body: body} ->
{:ok, Model.from_json(model, body)}
%HTTPoison.Response{status_code: 204} -> {:ok, nil}
%HTTPoison.Response{status_code: code, body: body}
%Dayron.Response{status_code: 204} -> {:ok, nil}
%Dayron.Response{status_code: code, body: body}
when code >= 400 and code < 500 ->
{:error, %{method: request.method, code: code, url: request.url,
response: body}}
Expand All @@ -355,10 +361,10 @@ defmodule Dayron.Repo do

defp handle_errors({request, response}, _opts) do
case response do
%HTTPoison.Response{status_code: 500, body: body} ->
%Dayron.Response{status_code: 500, body: body} ->
raise Dayron.ServerError, method: request.method, url: request.url,
body: body
%HTTPoison.Error{reason: reason} -> :ok
%Dayron.ClientError{reason: reason} -> :ok
raise Dayron.ClientError, method: request.method, url: request.url,
reason: reason
_ -> {request, response}
Expand Down
7 changes: 7 additions & 0 deletions lib/dayron/response.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Dayron.Response do
@moduledoc """
Defines a struct to store the response data returned by an api request.
"""
defstruct status_code: nil, body: nil, headers: []
@type t :: %__MODULE__{status_code: integer, body: binary, headers: list}
end
32 changes: 22 additions & 10 deletions test/lib/dayron/adapters/httpoison_adapter_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,23 @@ defmodule Dayron.HTTPoisonAdapterTest do
Plug.Conn.resp(conn, 200, ~s<{"name": "Full Name", "address":{"street": "Elm Street", "zipcode": "88888"}}>)
end
response = HTTPoisonAdapter.get("#{api_url}/resources/id")
assert {:ok, %HTTPoison.Response{status_code: 200, body: body}} = response
assert {:ok, %Dayron.Response{status_code: 200, body: body}} = response
assert body[:name] == "Full Name"
assert body[:address] == %{street: "Elm Street", zipcode: "88888"}
end

test "handles response body 'ok'", %{bypass: bypass, api_url: api_url} do
Bypass.expect bypass, fn conn ->
assert "/resources/id" == conn.request_path
assert [{"accept", "application/json"}, {"content-type", "application/json"} | _] = conn.req_headers
assert "GET" == conn.method
Plug.Conn.resp(conn, 200, "ok")
end
response = HTTPoisonAdapter.get("#{api_url}/resources/id")
assert {:ok, %Dayron.Response{status_code: 200, body: body}} = response
assert body == %{}
end

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
Expand All @@ -28,7 +40,7 @@ defmodule Dayron.HTTPoisonAdapterTest do
Plug.Conn.resp(conn, 200, ~s<[{"name": "First Resource"}, {"name": "Second Resource"}]>)
end
response = HTTPoisonAdapter.get("#{api_url}/resources")
assert {:ok, %HTTPoison.Response{status_code: 200, body: body}} = response
assert {:ok, %Dayron.Response{status_code: 200, body: body}} = response
[first, second | _t] = body
assert first[:name] == "First Resource"
assert second[:name] == "Second Resource"
Expand All @@ -43,7 +55,7 @@ defmodule Dayron.HTTPoisonAdapterTest do
Plug.Conn.resp(conn, 200, "")
end
response = HTTPoisonAdapter.get("#{api_url}/resources", [], [params: [{:q, "qu ery"}, {:page, 2}]])
assert {:ok, %HTTPoison.Response{status_code: 200, body: _}} = response
assert {:ok, %Dayron.Response{status_code: 200, body: _}} = response
end

test "accepts custom headers", %{bypass: bypass, api_url: api_url} do
Expand All @@ -55,7 +67,7 @@ defmodule Dayron.HTTPoisonAdapterTest do
Plug.Conn.resp(conn, 200, "")
end
response = HTTPoisonAdapter.get("#{api_url}/resources/id", [accesstoken: "token"])
assert {:ok, %HTTPoison.Response{status_code: 200, body: _}} = response
assert {:ok, %Dayron.Response{status_code: 200, body: _}} = response
end

test "returns a 404 response", %{bypass: bypass, api_url: api_url} do
Expand All @@ -65,7 +77,7 @@ defmodule Dayron.HTTPoisonAdapterTest do
Plug.Conn.resp(conn, 404, "")
end
response = HTTPoisonAdapter.get("#{api_url}/resources/invalid")
assert {:ok, %HTTPoison.Response{status_code: 404, body: _}} = response
assert {:ok, %Dayron.Response{status_code: 404, body: _}} = response
end

test "returns a 500 error response", %{bypass: bypass, api_url: api_url} do
Expand All @@ -75,12 +87,12 @@ defmodule Dayron.HTTPoisonAdapterTest do
Plug.Conn.resp(conn, 500, "")
end
response = HTTPoisonAdapter.get("#{api_url}/resources/server-error")
assert {:ok, %HTTPoison.Response{status_code: 500, body: _}} = response
assert {:ok, %Dayron.Response{status_code: 500, body: _}} = response
end

test "returns an error for invalid server" do
response = HTTPoisonAdapter.get("http://localhost:0001/resources/error")
assert {:error, %HTTPoison.Error{reason: :econnrefused}} = response
assert {:error, %Dayron.ClientError{reason: :econnrefused}} = response
end

test "returns a decoded body for a valid post request", %{bypass: bypass, api_url: api_url} do
Expand All @@ -91,7 +103,7 @@ defmodule Dayron.HTTPoisonAdapterTest do
Plug.Conn.resp(conn, 201, ~s<{"name": "Full Name", "age": 30}>)
end
response = HTTPoisonAdapter.post("#{api_url}/resources", %{name: "Full Name", age: 30})
assert {:ok, %HTTPoison.Response{status_code: 201, body: body}} = response
assert {:ok, %Dayron.Response{status_code: 201, body: body}} = response
assert body[:name] == "Full Name"
assert body[:age] == 30
end
Expand All @@ -104,7 +116,7 @@ defmodule Dayron.HTTPoisonAdapterTest do
Plug.Conn.resp(conn, 200, ~s<{"name": "Full Name", "age": 30}>)
end
response = HTTPoisonAdapter.patch("#{api_url}/resources/id", %{name: "Full Name", age: 30})
assert {:ok, %HTTPoison.Response{status_code: 200, body: body}} = response
assert {:ok, %Dayron.Response{status_code: 200, body: body}} = response
assert body[:name] == "Full Name"
assert body[:age] == 30
end
Expand All @@ -117,6 +129,6 @@ defmodule Dayron.HTTPoisonAdapterTest do
Plug.Conn.resp(conn, 204, "")
end
response = HTTPoisonAdapter.delete("#{api_url}/resources/id")
assert {:ok, %HTTPoison.Response{status_code: 204, body: nil}} = response
assert {:ok, %Dayron.Response{status_code: 204, body: nil}} = response
end
end
7 changes: 7 additions & 0 deletions test/lib/dayron/model_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,11 @@ defmodule Dayron.ModelTest do
Model.from_json(MyInvalidModel, %{name: "Full Name"})
end
end

test "raises on protocol exception on from_json_list" do
msg = ~r/the given module is not a Dayron.Model/
assert_raise Protocol.UndefinedError, msg, fn ->
Model.from_json_list(MyInvalidModel, [%{name: "Full Name"}])
end
end
end
3 changes: 2 additions & 1 deletion test/lib/dayron/repo_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ defmodule Dayron.RepoTest do
end

test "`get!` raises an exception for not found resource" do
assert_raise Dayron.NoResultsError, fn ->
msg = ~r/expected at least one result/
assert_raise Dayron.NoResultsError, msg, fn ->
TestRepo.get!(MyModel, "invalid-id")
end
end
Expand Down
Loading

0 comments on commit 87b7451

Please sign in to comment.