Add Endpoints behaviour and refactor router and related methods

This commit is contained in:
mitchell 2020-01-05 14:54:55 -05:00
parent 1502d10cdc
commit 98684feb2d
8 changed files with 98 additions and 62 deletions

11
dialyzer.ignore-warnings Normal file
View File

@ -0,0 +1,11 @@
:0: Unknown function 'Elixir.Shortnr.Transport.Text.Encodable.Atom':'__impl__'/1
:0: Unknown function 'Elixir.Shortnr.Transport.Text.Encodable.BitString':'__impl__'/1
:0: Unknown function 'Elixir.Shortnr.Transport.Text.Encodable.Float':'__impl__'/1
:0: Unknown function 'Elixir.Shortnr.Transport.Text.Encodable.Function':'__impl__'/1
:0: Unknown function 'Elixir.Shortnr.Transport.Text.Encodable.Integer':'__impl__'/1
:0: Unknown function 'Elixir.Shortnr.Transport.Text.Encodable.List':'__impl__'/1
:0: Unknown function 'Elixir.Shortnr.Transport.Text.Encodable.Map':'__impl__'/1
:0: Unknown function 'Elixir.Shortnr.Transport.Text.Encodable.PID':'__impl__'/1
:0: Unknown function 'Elixir.Shortnr.Transport.Text.Encodable.Port':'__impl__'/1
:0: Unknown function 'Elixir.Shortnr.Transport.Text.Encodable.Reference':'__impl__'/1
:0: Unknown function 'Elixir.Shortnr.Transport.Text.Encodable.Tuple':'__impl__'/1

View File

@ -3,57 +3,27 @@ defmodule Shortnr.Router do
This module contains the Router for the Shortnr application. Do not import, other than This module contains the Router for the Shortnr application. Do not import, other than
Application entry. Application entry.
""" """
use Plug.ErrorHandler alias Shortnr.Transport.HTTP
use Plug.Router alias Shortnr.URL
require Logger require Logger
alias Shortnr.Transport.{HTTP, Text} use Plug.ErrorHandler
alias Shortnr.URL use Plug.Router
plug(Plug.Logger, log: :debug) plug(Plug.Logger, log: :debug)
plug(:match) plug(:match)
plug(:dispatch) plug(:dispatch)
get "/" do # BEGIN URL routes
conn post("/urls/:url", do: URL.Endpoints.select(conn, :create, url))
|> HTTP.wrap()
|> HTTP.handle(fn -> URL.list(URL.Repo.ETS) end)
|> Text.encode_response()
|> HTTP.send(:ok)
end
post "/urls/:url" do get("/", do: URL.Endpoints.select(conn, :list))
conn get("/urls", do: URL.Endpoints.select(conn, :list))
|> HTTP.wrap(url) get("/:id", do: URL.Endpoints.select(conn, :get, id))
|> HTTP.handle(&URL.create(&1, URL.Repo.ETS))
|> Text.encode_response()
|> HTTP.send(:created)
end
get "/urls" do delete("/:id", do: URL.Endpoints.select(conn, :delete, id))
conn # END
|> HTTP.wrap()
|> HTTP.handle(fn -> URL.list(URL.Repo.ETS) end)
|> Text.encode_response()
|> HTTP.send(:ok)
end
get "/:id" do
conn
|> HTTP.wrap(id)
|> HTTP.handle(&URL.get(&1, URL.Repo.ETS))
|> Text.encode_response()
|> HTTP.send(:found)
end
delete "/:id" do
conn
|> HTTP.wrap(id)
|> HTTP.handle(&URL.delete(&1, URL.Repo.ETS))
|> Text.encode_response()
|> HTTP.send(:ok)
end
match _ do match _ do
conn conn

View File

@ -3,15 +3,13 @@ defmodule Shortnr do
The Shortnr application entry point. Check README for usage documenation. The Shortnr application entry point. Check README for usage documenation.
""" """
use Application
require Logger require Logger
use Application
@impl true @impl Application
def start(_type, _args) do def start(_type, _args) do
port = port()
children = [ children = [
{Plug.Cowboy, scheme: :http, plug: Shortnr.Router, options: [port: port]} {Plug.Cowboy, scheme: :http, plug: Shortnr.Router, options: [port: port()]}
] ]
if ets_implementation() == :dets do if ets_implementation() == :dets do
@ -20,11 +18,11 @@ defmodule Shortnr do
:ets.new(:urls, [:set, :named_table]) :ets.new(:urls, [:set, :named_table])
end end
Logger.info("server starting", port: port) Logger.info("server starting", port: port())
Supervisor.start_link(children, strategy: :one_for_one) Supervisor.start_link(children, strategy: :one_for_one)
end end
@impl true @impl Application
def stop(_state) do def stop(_state) do
if ets_implementation() == :dets, do: :dets.close(:urls) if ets_implementation() == :dets, do: :dets.close(:urls)
end end

View File

@ -64,3 +64,12 @@ defmodule Shortnr.Transport.HTTP do
defp status_to_code(:not_found), do: 404 defp status_to_code(:not_found), do: 404
defp status_to_code(:internal_server_error), do: 500 defp status_to_code(:internal_server_error), do: 500
end end
defmodule Shortnr.Transport.HTTP.Endpoints do
@moduledoc """
This modules is a behaviour that should be implemented by each service that needs to be routed by
Plug.Router
"""
@callback select(Plug.Conn.t(), atom, term) :: Plug.Conn.t()
end

View File

@ -3,28 +3,29 @@ defmodule Shortnr.Transport.Text do
This modules contains functions to decode and encode text formatted http requests and responses. This modules contains functions to decode and encode text formatted http requests and responses.
""" """
import Plug.Conn
alias Shortnr.Transport.HTTP alias Shortnr.Transport.HTTP
alias Shortnr.Transport.Text.Encodable alias Shortnr.Transport.Text.Encodable
@spec decode_request(Plug.Conn.t()) :: HTTP.ok_error() import Plug.Conn
def decode_request(conn) do
@spec decode(HTTP.ok_error()) :: HTTP.ok_error()
def decode({:ok, _body, conn}) do
{:ok, body, conn} = read_body(conn) {:ok, body, conn} = read_body(conn)
{:ok, body, conn} {:ok, body, conn}
end end
@spec encode_response(HTTP.ok_error()) :: HTTP.ok_error() @spec encode(HTTP.ok_error()) :: HTTP.ok_error()
def encode_response(ok_error = {:error, _, _}), do: ok_error def encode(ok_error = {:error, _, _}), do: ok_error
def encode_response({:ok, [], conn}) do def encode({:ok, [], conn}) do
{:ok, "", conn} {:ok, "", conn}
end end
def encode_response({:ok, body, conn}) when is_list(body) do def encode({:ok, body, conn}) when is_list(body) 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, body, conn}) do def encode({:ok, body, conn}) do
{:ok, Encodable.encode(body), conn} {:ok, Encodable.encode(body), conn}
end end
end end

45
lib/url/endpoints.ex Normal file
View File

@ -0,0 +1,45 @@
defmodule Shortnr.URL.Endpoints do
@moduledoc """
This module implements the Endpoints behaviour.
"""
alias Shortnr.Transport.{HTTP, Text}
alias Shortnr.Transport.HTTP.Endpoints
alias Shortnr.URL
@behaviour Endpoints
@impl Endpoints
def select(conn, name, arg \\ nil)
def select(conn, :list, _arg) do
conn
|> HTTP.wrap()
|> HTTP.handle(fn -> URL.list(URL.Repo.ETS) end)
|> Text.encode()
|> HTTP.send(:ok)
end
def select(conn, :create, url) do
conn
|> HTTP.wrap(url)
|> HTTP.handle(&URL.create(&1, URL.Repo.ETS))
|> Text.encode()
|> HTTP.send(:created)
end
def select(conn, :get, id) do
conn
|> HTTP.wrap(id)
|> HTTP.handle(&URL.get(&1, URL.Repo.ETS))
|> Text.encode()
|> HTTP.send(:found)
end
def select(conn, :delete, id) do
conn
|> HTTP.wrap(id)
|> HTTP.handle(&URL.delete(&1, URL.Repo.ETS))
|> Text.encode()
|> HTTP.send(:ok)
end
end

View File

@ -2,10 +2,11 @@ defmodule Shortnr.URL.Repo.ETS do
@moduledoc """ @moduledoc """
This module is an implemention of the Repo behaviour using the Erlang ETS library. This module is an implemention of the Repo behaviour using the Erlang ETS library.
""" """
alias Shortnr.URL.Repo
@behaviour Shortnr.URL.Repo @behaviour Repo
@impl true @impl Repo
def get(key) do def get(key) do
case ets().lookup(:urls, key) |> List.first() do case ets().lookup(:urls, key) |> List.first() do
{_, url} -> {:ok, url} {_, url} -> {:ok, url}
@ -13,25 +14,25 @@ defmodule Shortnr.URL.Repo.ETS do
end end
end end
@impl true @impl Repo
def put(url) do def put(url) do
:ok = ets().insert(:urls, {url.id, url}) :ok = ets().insert(:urls, {url.id, url})
:ok :ok
end end
@impl true @impl Repo
def list do def list do
resp = ets().select(:urls, [{:"$1", [], [:"$1"]}]) resp = ets().select(:urls, [{:"$1", [], [:"$1"]}])
{:ok, resp |> Enum.map(&elem(&1, 1))} {:ok, resp |> Enum.map(&elem(&1, 1))}
end end
@impl true @impl Repo
def delete(key) do def delete(key) do
:ok = ets().delete(:urls, key) :ok = ets().delete(:urls, key)
:ok :ok
end end
@impl true @impl Repo
def reset do def reset do
:ok = ets().delete_all_objects(:urls) :ok = ets().delete_all_objects(:urls)
end end

View File

@ -8,6 +8,7 @@ defmodule Shortnr.MixProject do
version: "0.1.0", version: "0.1.0",
elixir: "~> 1.9", elixir: "~> 1.9",
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
dialyzer: [ignore_warnings: "dialyzer.ignore-warnings"],
deps: deps() deps: deps()
] ]
end end