From 9ee9eddbfadf0a334c8c5011228bab420487078f Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 8 Dec 2019 13:44:04 -0500 Subject: [PATCH] First working iteration of shortnr api --- .gitignore | 3 +++ lib/router.ex | 33 +++++++++++++++++++++---------- lib/shortnr.ex | 2 ++ lib/transport/http.ex | 17 ++++++++++++++-- lib/transport/text.ex | 30 ++++++++++++++++++++++++++++ lib/url/repo/dets.ex | 20 +++++++++++++++++++ lib/url/repo/repo.ex | 8 ++++++++ lib/url/url.ex | 46 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 lib/transport/text.ex create mode 100644 lib/url/repo/dets.ex create mode 100644 lib/url/repo/repo.ex create mode 100644 lib/url/url.ex diff --git a/.gitignore b/.gitignore index 36ad98f..6eeff4b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ service-*.tar # Ignore elixir-ls artifacts /.elixir_ls/ + +# Ignore development DETS file +urls diff --git a/lib/router.ex b/lib/router.ex index 9cd7024..3f7b991 100644 --- a/lib/router.ex +++ b/lib/router.ex @@ -4,32 +4,45 @@ defmodule Shortnr.Router do require Logger - alias Shortnr.Transport.{Json, Http} - alias Shortnr.Url + alias Shortnr.Transport.{Text, Http} + alias Shortnr.URL plug(Plug.Logger, log: :debug) plug(:match) plug(:dispatch) - post "/urls" do - conn - |> Json.decode_request(Url.CreateRequest) - |> Http.handle(&Url.create(&1, Url.Repo.DETS)) - |> Json.encode_response() + post "/urls/:url" do + {:ok, url, conn} + |> Http.handle(&URL.create(&1, URL.Repo.DETS)) + |> Text.encode_response() |> Http.send(:created, conn) end + get "/urls" do + {:ok, :ignore, conn} + |> Http.handle(fn -> URL.list(URL.Repo.DETS) end) + |> Text.encode_response() + |> Http.send(:ok, conn) + end + + get "/:id" do + {:ok, id, conn} + |> Http.handle(&URL.get(&1, URL.Repo.DETS)) + |> Text.encode_response() + |> Http.send(:found, conn) + end + match _ do {:error, {:not_found, "route not found"}, conn} - |> Json.encode_response() + |> Text.encode_response() |> Http.send(:ignore, conn) end def handle_errors(conn, %{kind: _kind, reason: reason, stack: stack}) do - Logger.error(inspect(reason), stack: stack) + Logger.error(inspect(reason), stack: inspect(stack)) {:error, {:internal_server_error, "internal server error"}, conn} - |> Json.encode_response() + |> Text.encode_response() |> Http.send(:ignore, conn) end end diff --git a/lib/shortnr.ex b/lib/shortnr.ex index 54404a7..71d57d8 100644 --- a/lib/shortnr.ex +++ b/lib/shortnr.ex @@ -14,6 +14,8 @@ defmodule Shortnr do {Plug.Cowboy, scheme: :http, plug: Shortnr.Router, options: [port: port]} ] + :dets.open_file(:urls, type: :set) + Logger.info("server starting", port: port) Supervisor.start_link(children, strategy: :one_for_one) end diff --git a/lib/transport/http.ex b/lib/transport/http.ex index 383259e..ce36da1 100644 --- a/lib/transport/http.ex +++ b/lib/transport/http.ex @@ -4,16 +4,23 @@ defmodule Shortnr.Transport.Http do @type ok_error :: {:ok, term(), Plug.Conn.t()} | error() @type error :: {:error, {atom(), String.t()}, Plug.Conn.t()} - @spec handle(ok_error(), (term() -> ok_error())) :: ok_error() + @spec handle(ok_error(), (... -> ok_error())) :: ok_error() def handle(error = {:error, _sub_error, _conn}, _func), do: error - def handle({:ok, request, conn}, func) do + def handle({:ok, request, conn}, func) when is_function(func, 1) do case func.(request) do {:ok, response} -> {:ok, response, conn} {:error, error_value} -> convert_error(error_value, conn) end end + def handle({:ok, _request, conn}, func) when is_function(func, 0) do + case func.() do + {:ok, response} -> {:ok, response, conn} + {:error, error_value} -> convert_error(error_value, conn) + end + end + defp convert_error({:invalid_argument, message}, conn), do: {:error, {:bad_request, message}, conn} @@ -24,12 +31,18 @@ defmodule Shortnr.Transport.Http do send_resp(original_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)) + end + def send({:ok, body, conn}, success_status, _original_conn) do send_resp(conn, status_to_code(success_status), body) end defp status_to_code(:ok), do: 200 defp status_to_code(:created), do: 201 + defp status_to_code(:found), do: 302 defp status_to_code(:bad_request), do: 400 defp status_to_code(:not_found), do: 404 defp status_to_code(:internal_server_error), do: 500 diff --git a/lib/transport/text.ex b/lib/transport/text.ex new file mode 100644 index 0000000..7c74bd1 --- /dev/null +++ b/lib/transport/text.ex @@ -0,0 +1,30 @@ +defmodule Shortnr.Transport.Text do + import Plug.Conn + alias Shortnr.Transport.Http + alias Shortnr.URL + + @spec decode_request(Plug.Conn.t()) :: Http.ok_error() + def decode_request(conn) do + {:ok, body, conn} = read_body(conn) + {:ok, body, conn} + end + + @spec encode_response(Http.ok_error()) :: Http.ok_error() + def encode_response(ok_error = {:error, _, _}), do: ok_error + + def encode_response({:ok, [], conn}) do + {:ok, "", conn} + end + + def encode_response({:ok, body, conn}) when is_list(body) 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} + end +end diff --git a/lib/url/repo/dets.ex b/lib/url/repo/dets.ex new file mode 100644 index 0000000..3a598d1 --- /dev/null +++ b/lib/url/repo/dets.ex @@ -0,0 +1,20 @@ +defmodule Shortnr.URL.Repo.DETS do + @behaviour Shortnr.URL.Repo + + @impl true + def get(key) do + {:ok, :dets.lookup(:urls, key) |> List.first() |> elem(1)} + end + + @impl true + def put(url) do + :ok = :dets.insert(:urls, {url.id, url}) + :ok + end + + @impl true + def list() do + resp = :dets.select(:urls, [{:"$1", [], [:"$1"]}]) + {:ok, resp |> Enum.map(&elem(&1, 1))} + end +end diff --git a/lib/url/repo/repo.ex b/lib/url/repo/repo.ex new file mode 100644 index 0000000..620c4ea --- /dev/null +++ b/lib/url/repo/repo.ex @@ -0,0 +1,8 @@ +defmodule Shortnr.URL.Repo do + alias Shortnr.URL + alias Shortnr.Transport + + @callback put(URL.t()) :: :ok | Transport.error() + @callback get(String.t()) :: {:ok, URL.t()} | Transport.error() + @callback list() :: {:ok, list(URL.t())} | Transport.error() +end diff --git a/lib/url/url.ex b/lib/url/url.ex new file mode 100644 index 0000000..64cfcdf --- /dev/null +++ b/lib/url/url.ex @@ -0,0 +1,46 @@ +defmodule Shortnr.URL do + alias Shortnr.Transport + alias Shortnr.URL + + defstruct id: + for( + _ <- 0..7, + into: "", + do: + Enum.random( + String.codepoints( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXWYZ0123456789" + ) + ) + ), + created_at: DateTime.utc_now(), + updated_at: DateTime.utc_now(), + url: %URI{} + + @type t :: %__MODULE__{id: String.t(), url: URI.t()} + + @spec create(String.t(), module()) :: {:ok, String.t()} | Transport.error() + def create(url, repo) do + url_struct = %URL{url: URI.parse(url)} + :ok = repo.put(url_struct) + {:ok, url_struct.id} + end + + @spec get(String.t(), module()) :: {:ok, URL.t()} | Transport.error() + def get(key, repo) do + {:ok, _} = repo.get(key) + end + + @spec list(module()) :: {:ok, list(URL.t())} | Transport.error() + def list(repo) do + {:ok, _} = repo.list + 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}" + end + end +end