Add HTTP wrap func and Text Encodable:

- Refactor HTTP send func
- Impl. Text Encodable for URL
- Error when url is not found
This commit is contained in:
mitchell 2020-01-01 18:11:38 -05:00
parent b699e661e2
commit 1502d10cdc
8 changed files with 84 additions and 41 deletions

View File

@ -1,4 +1,4 @@
FROM elixir:1.9-slim as build FROM elixir:1.9 as build
WORKDIR /root/shortnr WORKDIR /root/shortnr
COPY . . COPY . .
@ -8,7 +8,7 @@ RUN mix local.rebar --force
RUN env MIX_ENV=prod mix release RUN env MIX_ENV=prod mix release
FROM debian:buster-20191014-slim FROM debian:stable-20191224-slim
WORKDIR /home/shortnr WORKDIR /home/shortnr
COPY --from=build /root/shortnr/_build/prod/rel/shortnr/ . COPY --from=build /root/shortnr/_build/prod/rel/shortnr/ .

View File

@ -19,6 +19,7 @@ config :logger, :console,
config :credo, config :credo,
checks: [ checks: [
# Ignore these checks because they don't apply to the projects Elixir version
{Credo.Check.Refactor.MapInto, false}, {Credo.Check.Refactor.MapInto, false},
{Credo.Check.Warning.LazyLogging, false} {Credo.Check.Warning.LazyLogging, false}
] ]

View File

@ -15,45 +15,57 @@ defmodule Shortnr.Router do
plug(:match) plug(:match)
plug(:dispatch) plug(:dispatch)
get "/" do
conn
|> HTTP.wrap()
|> HTTP.handle(fn -> URL.list(URL.Repo.ETS) end)
|> Text.encode_response()
|> HTTP.send(:ok)
end
post "/urls/:url" do post "/urls/:url" do
{:ok, url, conn} conn
|> HTTP.wrap(url)
|> HTTP.handle(&URL.create(&1, URL.Repo.ETS)) |> HTTP.handle(&URL.create(&1, URL.Repo.ETS))
|> Text.encode_response() |> Text.encode_response()
|> HTTP.send(:created, conn) |> HTTP.send(:created)
end end
get "/urls" do get "/urls" do
{:ok, :ignore, conn} conn
|> HTTP.wrap()
|> HTTP.handle(fn -> URL.list(URL.Repo.ETS) end) |> HTTP.handle(fn -> URL.list(URL.Repo.ETS) end)
|> Text.encode_response() |> Text.encode_response()
|> HTTP.send(:ok, conn) |> HTTP.send(:ok)
end end
get "/:id" do get "/:id" do
{:ok, id, conn} conn
|> HTTP.wrap(id)
|> HTTP.handle(&URL.get(&1, URL.Repo.ETS)) |> HTTP.handle(&URL.get(&1, URL.Repo.ETS))
|> Text.encode_response() |> Text.encode_response()
|> HTTP.send(:found, conn) |> HTTP.send(:found)
end end
delete "/:id" do delete "/:id" do
{:ok, id, conn} conn
|> HTTP.wrap(id)
|> HTTP.handle(&URL.delete(&1, URL.Repo.ETS)) |> HTTP.handle(&URL.delete(&1, URL.Repo.ETS))
|> Text.encode_response() |> Text.encode_response()
|> HTTP.send(:ok, conn) |> HTTP.send(:ok)
end end
match _ do match _ do
{:error, {:not_found, "route not found"}, conn} conn
|> Text.encode_response() |> HTTP.wrap({:not_found, "route not found"})
|> HTTP.send(:ignore, conn) |> HTTP.send()
end end
def handle_errors(conn, %{kind: _kind, reason: reason, stack: stack}) do def handle_errors(conn, %{kind: _kind, reason: reason, stack: stack}) do
Logger.error(reason, stack: stack) Logger.error(inspect(reason), stack: ~s|"#{inspect(stack)}"|)
{:error, {:internal_server_error, "internal server error"}, conn} conn
|> Text.encode_response() |> HTTP.wrap({:internal_server_error, "internal server error"})
|> HTTP.send(:ignore, conn) |> HTTP.send()
end end
end end

View File

@ -3,13 +3,20 @@ defmodule Shortnr.Transport.HTTP do
This module contains functions that can be used to handle HTTP requests and send responses, by This module contains functions that can be used to handle HTTP requests and send responses, by
manipulating Plug.Conn. manipulating Plug.Conn.
""" """
alias Shortnr.Transport
import Plug.Conn import Plug.Conn
@type error :: {:error, {atom(), String.t()}, Plug.Conn.t()} @type error :: {:error, {atom(), String.t()}, Plug.Conn.t()}
@type ok_error :: {:ok, term(), Plug.Conn.t()} | error() @type ok_error :: {:ok, term(), Plug.Conn.t()} | error()
@spec handle(ok_error(), (... -> ok_error())) :: ok_error() @spec wrap(Plug.Conn.t(), term()) :: ok_error
def wrap(conn, argument \\ nil)
def wrap(conn, nil), do: {:ok, nil, conn}
def wrap(conn, {first, _second} = argument) when is_atom(first), do: {:error, argument, conn}
def wrap(conn, argument), do: {:ok, argument, conn}
@spec handle(ok_error(), (term() -> Transport.ok_error()) | (() -> Transport.ok_error())) ::
ok_error()
def handle(error = {:error, _sub_error, _conn}, _func), do: error def handle(error = {:error, _sub_error, _conn}, _func), do: error
def handle({:ok, request, conn}, func) when is_function(func, 1) do def handle({:ok, request, conn}, func) when is_function(func, 1) do
@ -29,19 +36,24 @@ defmodule Shortnr.Transport.HTTP do
defp convert_error({:invalid_argument, message}, conn), defp convert_error({:invalid_argument, message}, conn),
do: {:error, {:bad_request, message}, conn} do: {:error, {:bad_request, message}, conn}
defp convert_error({:not_found, message}, conn),
do: {:error, {:not_found, message}, conn}
defp convert_error(_, conn), do: {:error, {:internal_server_error, "unknown error"}, conn} defp convert_error(_, conn), do: {:error, {:internal_server_error, "unknown error"}, conn}
@spec send(ok_error(), atom(), Plug.Conn.t()) :: Plug.Conn.t() @spec send(ok_error(), atom()) :: Plug.Conn.t()
def send({:error, {status, body}, _conn}, _success_status, original_conn) do def send(ok_error, success_status \\ nil)
send_resp(original_conn, status_to_code(status), body)
def send({:error, {status, body}, conn}, _success_status) do
send_resp(conn, status_to_code(status), body)
end end
def send({:ok, body, conn}, :found, _original_conn) do def send({:ok, location, conn}, :found) do
conn = put_resp_header(conn, "Location", URI.to_string(body)) conn = put_resp_header(conn, "Location", location)
send_resp(conn, status_to_code(:found), URI.to_string(body)) send_resp(conn, status_to_code(:found), location)
end end
def send({:ok, body, conn}, success_status, _original_conn) do def send({:ok, body, conn}, success_status) do
send_resp(conn, status_to_code(success_status), body) send_resp(conn, status_to_code(success_status), body)
end end

View File

@ -5,7 +5,7 @@ defmodule Shortnr.Transport.Text do
import Plug.Conn import Plug.Conn
alias Shortnr.Transport.HTTP alias Shortnr.Transport.HTTP
alias Shortnr.URL alias Shortnr.Transport.Text.Encodable
@spec decode_request(Plug.Conn.t()) :: HTTP.ok_error() @spec decode_request(Plug.Conn.t()) :: HTTP.ok_error()
def decode_request(conn) do def decode_request(conn) do
@ -24,11 +24,20 @@ defmodule Shortnr.Transport.Text do
{:ok, for(item <- body, into: "", do: "#{item}\n"), conn} {:ok, for(item <- body, into: "", do: "#{item}\n"), conn}
end end
def encode_response({:ok, %URL{url: url}, conn}) do
{:ok, url, conn}
end
def encode_response({:ok, body, conn}) do def encode_response({:ok, body, conn}) do
{:ok, "#{body}", conn} {:ok, Encodable.encode(body), conn}
end end
end end
defprotocol Shortnr.Transport.Text.Encodable do
@moduledoc """
Implement this protocol for your type if you would like to text encode it.
"""
@fallback_to_any true
def encode(encodable)
end
defimpl Shortnr.Transport.Text.Encodable, for: Any do
def encode(encodable), do: to_string(encodable)
end

View File

@ -20,21 +20,24 @@ defmodule Shortnr.URL do
@spec create(String.t(), module()) :: {:ok, String.t()} | Transport.error() @spec create(String.t(), module()) :: {:ok, String.t()} | Transport.error()
def create(url, repo) do def create(url, repo) do
url_struct = %__MODULE__{id: Util.gen_id(), url: URI.parse(url)} url_struct = %__MODULE__{id: Util.generate_id(), url: URI.parse(url)}
{:ok, extant_url} = repo.get(url_struct.id) {:ok, extant_url} = repo.get(url_struct.id)
if is_nil(extant_url) do if extant_url do
create(url, repo)
else
:ok = repo.put(url_struct) :ok = repo.put(url_struct)
{:ok, url_struct.id} {:ok, url_struct.id}
else
create(url, repo)
end end
end end
@spec get(String.t(), module()) :: {:ok, t()} | Transport.error() @spec get(String.t(), module()) :: {:ok, t()} | Transport.error()
def get(key, repo) do def get(key, repo) do
{:ok, _} = repo.get(key) case repo.get(key) do
{:ok, nil} -> {:error, {:not_found, "url could not be found with the given id"}}
{:ok, url} -> {:ok, url}
end
end end
@spec list(module()) :: {:ok, list(t())} | Transport.error() @spec list(module()) :: {:ok, list(t())} | Transport.error()
@ -48,11 +51,17 @@ defmodule Shortnr.URL do
{:ok, "Success"} {:ok, "Success"}
end end
defimpl Transport.Text.Encodable do
alias Shortnr.URL
@spec encode(URL.t()) :: String.t()
def encode(url), do: URI.to_string(url.url)
end
defimpl String.Chars do defimpl String.Chars do
alias Shortnr.URL alias Shortnr.URL
@spec to_string(URL.t()) :: String.t() @spec to_string(URL.t()) :: String.t()
def to_string(url) do def to_string(url) do
"id=#{url.id} created=#{url.created_at} updated=#{url.updated_at} url=#{url.url}" "id=#{url.id} created_at=#{url.created_at} updated_at=#{url.updated_at} url=#{url.url}"
end end
end end
end end

View File

@ -3,8 +3,8 @@ defmodule Shortnr.URL.Util do
URL module utility functions. URL module utility functions.
""" """
@id_chars String.codepoints("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXWYZ0123456789") @id_chars String.codepoints("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXWYZ0123456789")
@spec gen_id() :: String.t() @spec generate_id() :: String.t()
def gen_id do def generate_id do
for _ <- 0..7, for _ <- 0..7,
into: "", into: "",
do: Enum.random(@id_chars) do: Enum.random(@id_chars)

View File

@ -4,6 +4,7 @@ defmodule Shortnr.MixProject do
def project do def project do
[ [
app: :shortnr, app: :shortnr,
description: "A small & simple url shortener",
version: "0.1.0", version: "0.1.0",
elixir: "~> 1.9", elixir: "~> 1.9",
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
@ -24,7 +25,6 @@ defmodule Shortnr.MixProject do
[ [
{:plug_cowboy, "~> 2.0"}, {:plug_cowboy, "~> 2.0"},
{:elixir_uuid, "~> 1.2"}, {:elixir_uuid, "~> 1.2"},
{:jason, "~> 1.1"},
{:dialyxir, "~> 0.5.1", only: [:dev], runtime: false}, {:dialyxir, "~> 0.5.1", only: [:dev], runtime: false},
{:credo, "~> 1.1.5", only: [:dev, :test], runtime: false} {:credo, "~> 1.1.5", only: [:dev, :test], runtime: false}
# {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_hexpm, "~> 0.3.0"},