From 1502d10cdc3a1ebacd18a71466fabc081eb4a585 Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 1 Jan 2020 18:11:38 -0500 Subject: [PATCH] Add HTTP wrap func and Text Encodable: - Refactor HTTP send func - Impl. Text Encodable for URL - Error when url is not found --- Dockerfile | 4 ++-- config/config.exs | 1 + lib/router.ex | 42 +++++++++++++++++++++++++++--------------- lib/transport/http.ex | 30 +++++++++++++++++++++--------- lib/transport/text.ex | 21 +++++++++++++++------ lib/url/url.ex | 21 +++++++++++++++------ lib/url/util.ex | 4 ++-- mix.exs | 2 +- 8 files changed, 84 insertions(+), 41 deletions(-) diff --git a/Dockerfile b/Dockerfile index a10ca6b..e70adff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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/ . diff --git a/config/config.exs b/config/config.exs index f875974..f7a55f8 100644 --- a/config/config.exs +++ b/config/config.exs @@ -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} ] diff --git a/lib/router.ex b/lib/router.ex index 5becef8..6dd3fe5 100644 --- a/lib/router.ex +++ b/lib/router.ex @@ -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 diff --git a/lib/transport/http.ex b/lib/transport/http.ex index aa81d69..1e1084e 100644 --- a/lib/transport/http.ex +++ b/lib/transport/http.ex @@ -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 diff --git a/lib/transport/text.ex b/lib/transport/text.ex index 4ce481f..b153c47 100644 --- a/lib/transport/text.ex +++ b/lib/transport/text.ex @@ -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} - end - def encode_response({:ok, body, conn}) do - {:ok, "#{body}", conn} + {:ok, Encodable.encode(body), conn} 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 diff --git a/lib/url/url.ex b/lib/url/url.ex index a74ed93..73ebaa3 100644 --- a/lib/url/url.ex +++ b/lib/url/url.ex @@ -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 diff --git a/lib/url/util.ex b/lib/url/util.ex index 73fd350..846e462 100644 --- a/lib/url/util.ex +++ b/lib/url/util.ex @@ -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) diff --git a/mix.exs b/mix.exs index c0f5ba1..707240d 100644 --- a/mix.exs +++ b/mix.exs @@ -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"},