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
COPY . .
@ -8,7 +8,7 @@ RUN mix local.rebar --force
RUN env MIX_ENV=prod mix release
FROM debian:buster-20191014-slim
FROM debian:stable-20191224-slim
WORKDIR /home/shortnr
COPY --from=build /root/shortnr/_build/prod/rel/shortnr/ .

View File

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

View File

@ -15,45 +15,57 @@ defmodule Shortnr.Router do
plug(:match)
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
{:ok, url, conn}
conn
|> HTTP.wrap(url)
|> HTTP.handle(&URL.create(&1, URL.Repo.ETS))
|> Text.encode_response()
|> HTTP.send(:created, conn)
|> HTTP.send(:created)
end
get "/urls" do
{:ok, :ignore, conn}
conn
|> HTTP.wrap()
|> HTTP.handle(fn -> URL.list(URL.Repo.ETS) end)
|> Text.encode_response()
|> HTTP.send(:ok, conn)
|> HTTP.send(:ok)
end
get "/:id" do
{:ok, id, conn}
conn
|> HTTP.wrap(id)
|> HTTP.handle(&URL.get(&1, URL.Repo.ETS))
|> Text.encode_response()
|> HTTP.send(:found, conn)
|> HTTP.send(:found)
end
delete "/:id" do
{:ok, id, conn}
conn
|> HTTP.wrap(id)
|> HTTP.handle(&URL.delete(&1, URL.Repo.ETS))
|> Text.encode_response()
|> HTTP.send(:ok, conn)
|> HTTP.send(:ok)
end
match _ do
{:error, {:not_found, "route not found"}, conn}
|> Text.encode_response()
|> HTTP.send(:ignore, conn)
conn
|> HTTP.wrap({:not_found, "route not found"})
|> HTTP.send()
end
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}
|> Text.encode_response()
|> HTTP.send(:ignore, conn)
conn
|> HTTP.wrap({:internal_server_error, "internal server error"})
|> HTTP.send()
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
manipulating Plug.Conn.
"""
alias Shortnr.Transport
import Plug.Conn
@type error :: {:error, {atom(), String.t()}, Plug.Conn.t()}
@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({: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),
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}
@spec send(ok_error(), atom(), Plug.Conn.t()) :: Plug.Conn.t()
def send({:error, {status, body}, _conn}, _success_status, original_conn) do
send_resp(original_conn, status_to_code(status), body)
@spec send(ok_error(), atom()) :: Plug.Conn.t()
def send(ok_error, success_status \\ nil)
def send({:error, {status, body}, conn}, _success_status) do
send_resp(conn, status_to_code(status), body)
end
def send({:ok, body, conn}, :found, _original_conn) do
conn = put_resp_header(conn, "Location", URI.to_string(body))
send_resp(conn, status_to_code(:found), URI.to_string(body))
def send({:ok, location, conn}, :found) do
conn = put_resp_header(conn, "Location", location)
send_resp(conn, status_to_code(:found), location)
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)
end

View File

@ -5,7 +5,7 @@ defmodule Shortnr.Transport.Text do
import Plug.Conn
alias Shortnr.Transport.HTTP
alias Shortnr.URL
alias Shortnr.Transport.Text.Encodable
@spec decode_request(Plug.Conn.t()) :: HTTP.ok_error()
def decode_request(conn) do
@ -24,11 +24,20 @@ defmodule Shortnr.Transport.Text do
{:ok, for(item <- body, into: "", do: "#{item}\n"), conn}
end
def encode_response({:ok, %URL{url: url}, conn}) do
{:ok, url, conn}
def encode_response({:ok, body, conn}) do
{:ok, Encodable.encode(body), conn}
end
end
def encode_response({:ok, body, conn}) do
{:ok, "#{body}", conn}
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()
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)
if is_nil(extant_url) do
if extant_url do
create(url, repo)
else
:ok = repo.put(url_struct)
{:ok, url_struct.id}
else
create(url, repo)
end
end
@spec get(String.t(), module()) :: {:ok, t()} | Transport.error()
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
@spec list(module()) :: {:ok, list(t())} | Transport.error()
@ -48,11 +51,17 @@ defmodule Shortnr.URL do
{:ok, "Success"}
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
alias Shortnr.URL
@spec to_string(URL.t()) :: String.t()
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

View File

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

View File

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