Skip to content

Commit

Permalink
[#21] repo.update action, adapter and httpoison_adapter patch method
Browse files Browse the repository at this point in the history
  • Loading branch information
flaviogranero committed May 10, 2016
1 parent 359e330 commit d4238ac
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 19 deletions.
31 changes: 31 additions & 0 deletions lib/dayron/adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,35 @@ defmodule Dayron.Adapter do
Check the adapter implementations for default values.
"""
@callback post(binary, body, headers, Keyword.t) :: response

@doc """
Issues a PATCH request to the given url.
Returns `{:ok, response}` if the request is successful,
`{:error, reason}` otherwise.
## Arguments:
* `url` - target url as a binary string or char list
* `body` - request body. Usually a struct deriving `Poison.Encoder`
* `headers` - HTTP headers as an orddict
(e.g., `[{"Accept", "application/json"}]`)
* `options` - Keyword list of options
## Options:
* `:timeout` - timeout to establish a connection, in milliseconds.
* `:recv_timeout` - timeout used when receiving a connection.
* `:stream_to` - a PID to stream the response to
* `:proxy` - a proxy to be used for the request;
it can be a regular url or a `{Host, Proxy}` tuple
* `:proxy_auth` - proxy authentication `{User, Password}` tuple
* `:ssl` - SSL options supported by the `ssl` erlang module
* `:follow_redirect` - a boolean that causes redirects to be followed
* `:max_redirect` - the maximum number of redirects to follow
* `:params` - an enumerable consisting of two-item tuples that will be
appended to the url as query string parameters
Timeouts can be an integer or `:infinity`.
Check the adapter implementations for default values.
"""
@callback patch(binary, body, headers, Keyword.t) :: response
end
8 changes: 8 additions & 0 deletions lib/dayron/adapters/httpoison_adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,12 @@ defmodule Dayron.HTTPoisonAdapter do
Client.start
Client.post(url, body, headers, opts)
end

@doc """
Implementation for `Dayron.Adapter.patch/4`.
"""
def patch(url, body, headers \\ [], opts \\ []) do
Client.start
Client.patch(url, body, headers, opts)
end
end
106 changes: 88 additions & 18 deletions lib/dayron/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,16 @@ defmodule Dayron.Repo do
Repo.insert!(@adapter, model, data, opts, @config)
end

# TBD
def update(model, opts \\ []), do: nil
def update(model, id, data, opts \\ []) do
Repo.update(@adapter, model, id, data, opts, @config)
end

def delete(model, opts \\ []), do: nil
def update!(model, id, data, opts \\ []) do
Repo.update!(@adapter, model, id, data, opts, @config)
end

def update!(model, opts \\ []), do: nil
# TBD
def delete(model, opts \\ []), do: nil

def delete!(model, opts \\ []), do: nil
end
Expand Down Expand Up @@ -129,15 +133,7 @@ defmodule Dayron.Repo do
end

@doc """
Inserts a model or an `Ecto.Changeset`.
In case a model is given, the model is converted into a changeset
with all model non-virtual fields as part of the changeset.
This conversion is done by calling `Ecto.Changeset.change/2` directly.
In case a changeset is given, the changes in the changeset are
merged with the model fields, and all of them are sent to the
database.
Inserts a model given a map with resource attributes.
Options are sent directly to the selected adapter.
See Dayron.Adapter.insert/3 for avaliable options.
Expand All @@ -149,9 +145,9 @@ defmodule Dayron.Repo do
## Example
case RestRepo.insert %User{name: "Dayse"} do
{:ok, model} -> # Inserted with success
{:error, changeset} -> # Something went wrong
case RestRepo.insert User, %{name: "Dayse"} do
{:ok, model} -> # Inserted with success
{:error, error} -> # Something went wrong
end
"""
Expand All @@ -167,6 +163,39 @@ defmodule Dayron.Repo do
raise @cannot_call_directly_error
end

@doc """
Updates a model given an id and a map with resource attributes.
Options are sent directly to the selected adapter.
See Dayron.Adapter.insert/3 for avaliable options.
## Possible Exceptions
* `Dayron.ServerError` - if server responds with a 500 internal error.
* `Dayron.ClientError` - for any error detected in client side, such as
timeout or connection errors.
## Example
case RestRepo.update User, "user-id", %{name: "Dayse"} do
{:ok, model} -> # Updated with success
{:error, error} -> # Something went wrong
end
"""
def update(_module, _id, _data, _opts \\ []) do
raise @cannot_call_directly_error
end

@doc """
Similar to `insert/4` but raises:
* `Dayron.NoResultsError` - if server responds with 404 resource not found.
* `Dayron.ValidationError` - if server responds with 422 unprocessable
entity.
"""
def update!(_module, _id, _data, _opts \\ []) do
raise @cannot_call_directly_error
end

@doc false
def get(adapter, model, id, opts, config) do
{url, response} = get_response(adapter, model, [id: id], opts, config)
Expand Down Expand Up @@ -220,19 +249,49 @@ defmodule Dayron.Repo do
end
end

@doc false
def insert!(adapter, model, data, opts, config) do
case insert(adapter, model, data, opts, config) do
{:ok, model} -> {:ok, model}
{:error, error} -> raise Dayron.ValidationError, Map.to_list(error)
end
end

@doc false
def update(adapter, model, id, data, opts, config) do
{url, response} = patch_response(adapter, model, [id: id], data, opts,
config)
case response do
%HTTPoison.Response{status_code: 200, body: body} ->
{:ok, Model.from_json(model, body)}
%HTTPoison.Response{status_code: code, body: body}
when code >= 400 and code < 500 ->
{:error, %{method: "PATCH", code: code, url: url, response: body}}
%HTTPoison.Response{status_code: code, body: body} when code >= 500 ->
raise Dayron.ServerError, method: "PATCH", url: url, body: body
%HTTPoison.Error{reason: reason} ->
raise Dayron.ClientError, method: "PATCH", url: url, reason: reason
end
end

@doc false
def update!(adapter, model, id, data, opts, config) do
case update(adapter, model, id, data, opts, config) do
{:ok, model} -> {:ok, model}
{:error, %{code: 404} = error} ->
raise Dayron.NoResultsError, Map.to_list(error)
{:error, %{code: 422} = error} ->
raise Dayron.ValidationError, Map.to_list(error)
end
end

defp get_response(adapter, model, url_opts, request_opts, config) do
url = Config.get_request_url(config, model, url_opts)
headers = Config.get_headers(config)
{_, response} = adapter.get(url, headers, request_opts)
if Config.log_responses?(config) do
ResponseLogger.log("GET", url, response)
request_details = [params: url_opts]
ResponseLogger.log("GET", url, response, request_details)
end
{url, response}
end
Expand All @@ -242,9 +301,20 @@ defmodule Dayron.Repo do
headers = Config.get_headers(config)
{_, response} = adapter.post(url, data, headers, request_opts)
if Config.log_responses?(config) do
request_details = [body: model]
request_details = [body: data]
ResponseLogger.log("POST", url, response, request_details)
end
{url, response}
end

defp patch_response(adapter, model, url_opts, data, request_opts, config) do
url = Config.get_request_url(config, model, url_opts)
headers = Config.get_headers(config)
{_, response} = adapter.patch(url, data, headers, request_opts)
if Config.log_responses?(config) do
request_details = [body: data]
ResponseLogger.log("PATCH", url, response, request_details)
end
{url, response}
end
end
13 changes: 13 additions & 0 deletions test/lib/dayron/adapters/httpoison_adapter_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ defmodule Dayron.HTTPoisonAdapterTest do
assert body[:age] == 30
end

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"}, {"content-type", "application/json"} | _] = conn.req_headers
assert "PATCH" == conn.method
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 body[:name] == "Full Name"
assert body[:age] == 30
end

test "accepts custom headers", %{bypass: bypass, api_url: api_url} do
Bypass.expect bypass, fn conn ->
assert "/resources/id" == conn.request_path
Expand Down
82 changes: 82 additions & 0 deletions test/lib/dayron/repo_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,86 @@ defmodule Dayron.RepoTest do
Dayron.Repo.insert!(MyModel, %{})
end
end

# ================ UPDATE ===========================
test "`update` a valid resource given model and data" do
data = %{name: "Full Name", age: 30}
{:ok, %MyModel{} = model} = TestRepo.update(MyModel, 'id', data)
assert model.id == "updated-model-id"
end

test "`update` a valid resource fails when data is invalid" do
data = %{name: nil, age: 30}
{:error, %{method: "PATCH", code: 422, response: response}} = TestRepo.update(MyModel, 'id', data)
assert response[:error] == "name is required"
end

test "`update` an invalid resource returns an error" do
data = %{name: "Full Name", age: 30}
{:error, %{method: "PATCH", code: 404}} = TestRepo.update(MyModel, 'invalid-id', data)
end

test "`update` raises an exception on request error" do
msg = ~r/Internal Exception/
assert_raise Dayron.ServerError, msg, fn ->
TestRepo.update(MyModel, 'id', %{error: "server-error"})
end
end

test "`update` raises an exception on connection error" do
msg = ~r/econnrefused/
assert_raise Dayron.ClientError, msg, fn ->
TestRepo.update(MyModel, 'id', %{error: "connection-error"})
end
end

test "`update` does not accept direct Dayron.Repo.update call" do
msg = ~r/Cannot call Dayron.Repo directly/
assert_raise RuntimeError, msg, fn ->
Dayron.Repo.update(MyModel, 'id', %{})
end
end

# ================ INSERT! ===========================
test "`update!` a valid resource given a model and data" do
data = %{name: "Full Name", age: 30}
{:ok, model = %MyModel{}} = TestRepo.update!(MyModel, 'id', data)
assert model.id == "updated-model-id"
end

test "`update!` raises an exception when data is an invalid model" do
data = %{name: nil, age: 30}
msg = ~r/validation error/
assert_raise Dayron.ValidationError, msg, fn ->
TestRepo.update!(MyModel, 'id', data)
end
end

test "`update!` raises an exception when id is invalid" do
data = %{name: "Full Name", age: 30}
assert_raise Dayron.NoResultsError, fn ->
TestRepo.update!(MyModel, 'invalid-id', data)
end
end

test "`update!` raises an exception on request error" do
msg = ~r/Internal Exception/
assert_raise Dayron.ServerError, msg, fn ->
TestRepo.update!(MyModel, 'id', %{error: "server-error"})
end
end

test "`update!` raises an exception on connection error" do
msg = ~r/econnrefused/
assert_raise Dayron.ClientError, msg, fn ->
TestRepo.update!(MyModel, 'id', %{error: "connection-error"})
end
end

test "`update!` does not accept direct Dayron.Repo.update! call" do
msg = ~r/Cannot call Dayron.Repo directly/
assert_raise RuntimeError, msg, fn ->
Dayron.Repo.update!(MyModel, 'id', %{})
end
end
end
21 changes: 20 additions & 1 deletion test/support/test_repo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,26 @@ defmodule Dayron.TestAdapter do
{:ok, %HTTPoison.Response{status_code: 201, body: model}}
end

%HTTPoison.Error{reason: :connect_timeout}
def patch("http://localhost/resources/id", %{name: nil}, [], []) do
{:ok, %HTTPoison.Response{status_code: 422, body: %{error: "name is required"}}}
end

def patch("http://localhost/resources/id", %{error: "server-error"}, [], []) do
{:ok, %HTTPoison.Response{status_code: 500, body: "Internal Exception..."}}
end

def patch("http://localhost/resources/id", %{error: "connection-error"}, [], []) do
{:error, %HTTPoison.Error{id: nil, reason: :econnrefused}}
end

def patch("http://localhost/resources/invalid-id", _, [], []) do
{:ok, %HTTPoison.Response{status_code: 404, body: ""}}
end

def patch("http://localhost/resources/id", model, [], []) do
model = Map.put(model, :id, "updated-model-id")
{:ok, %HTTPoison.Response{status_code: 200, body: model}}
end
end

Application.put_env(:dayron, Dayron.TestRepo, [url: "http://localhost", enable_log: false])
Expand Down

0 comments on commit d4238ac

Please sign in to comment.