diff --git a/Dockerfile b/Dockerfile index 028aac7..a10ca6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN env MIX_ENV=prod mix release FROM debian:buster-20191014-slim WORKDIR /home/shortnr -COPY --from=build /root/shortnr/_build/prod/rel/service/ . +COPY --from=build /root/shortnr/_build/prod/rel/shortnr/ . RUN apt-get update && apt-get install -y --no-install-recommends \ libtinfo5=6.1+20181013-2+deb10u2 \ @@ -29,5 +29,5 @@ RUN groupadd -r shortnr && useradd --no-log-init -r -g shortnr shortnr RUN chown -R shortnr:shortnr /home/shortnr USER shortnr -ENTRYPOINT ["bin/service", "start"] +ENTRYPOINT ["bin/shortnr", "start"] EXPOSE 8080 diff --git a/config/config.exs b/config/config.exs index ceceb7a..0a76b38 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,7 +1,12 @@ import Config -config :service, - port: 8080 +config :shortnr, + port: 8080, + ets_implementation: + (fn + :test -> :ets + _ -> :dets + end).(Mix.env()) config :logger, :console, format: "date=$date time=$time level=$level$levelpad message=\"$message\" $metadata\n", diff --git a/lib/router.ex b/lib/router.ex index 5778a3e..e62c785 100644 --- a/lib/router.ex +++ b/lib/router.ex @@ -4,7 +4,7 @@ defmodule Shortnr.Router do require Logger - alias Shortnr.Transport.{Text, Http} + alias Shortnr.Transport.{Text, HTTP} alias Shortnr.URL plug(Plug.Logger, log: :debug) @@ -13,36 +13,36 @@ defmodule Shortnr.Router do post "/urls/:url" do {:ok, url, conn} - |> Http.handle(&URL.create(&1, URL.Repo.DETS)) + |> HTTP.handle(&URL.create(&1, URL.Repo.ETS)) |> Text.encode_response() - |> Http.send(:created, conn) + |> HTTP.send(:created, conn) end get "/urls" do {:ok, :ignore, conn} - |> Http.handle(fn -> URL.list(URL.Repo.DETS) end) + |> HTTP.handle(fn -> URL.list(URL.Repo.ETS) end) |> Text.encode_response() - |> Http.send(:ok, conn) + |> HTTP.send(:ok, conn) end get "/:id" do {:ok, id, conn} - |> Http.handle(&URL.get(&1, URL.Repo.DETS)) + |> HTTP.handle(&URL.get(&1, URL.Repo.ETS)) |> Text.encode_response() - |> Http.send(:found, conn) + |> HTTP.send(:found, conn) end delete "/:id" do {:ok, id, conn} - |> Http.handle(&URL.delete(&1, URL.Repo.DETS)) + |> HTTP.handle(&URL.delete(&1, URL.Repo.ETS)) |> Text.encode_response() - |> Http.send(:ok, conn) + |> HTTP.send(:ok, conn) end match _ do {:error, {:not_found, "route not found"}, conn} |> Text.encode_response() - |> Http.send(:ignore, conn) + |> HTTP.send(:ignore, conn) end def handle_errors(conn, %{kind: _kind, reason: reason, stack: stack}) do @@ -50,6 +50,6 @@ defmodule Shortnr.Router do {:error, {:internal_server_error, "internal server error"}, conn} |> Text.encode_response() - |> Http.send(:ignore, conn) + |> HTTP.send(:ignore, conn) end end diff --git a/lib/shortnr.ex b/lib/shortnr.ex index 71d57d8..96257d1 100644 --- a/lib/shortnr.ex +++ b/lib/shortnr.ex @@ -14,17 +14,29 @@ defmodule Shortnr do {Plug.Cowboy, scheme: :http, plug: Shortnr.Router, options: [port: port]} ] - :dets.open_file(:urls, type: :set) + if ets_implementation() == :dets do + {:ok, _} = :dets.open_file(:urls, type: :set) + else + :ets.new(:urls, [:set, :named_table]) + end Logger.info("server starting", port: port) Supervisor.start_link(children, strategy: :one_for_one) end + @impl true + def stop(_state) do + if ets_implementation() == :dets, do: :dets.close(:urls) + end + @spec port() :: integer() defp port do - case Application.fetch_env(:service, :port) do + case Application.fetch_env(:shortnr, :port) do {:ok, port} -> port _ -> 4000 end end + + @spec ets_implementation() :: atom() + defp ets_implementation, do: Application.fetch_env!(:shortnr, :ets_implementation) end diff --git a/lib/transport/http.ex b/lib/transport/http.ex index ce36da1..f976db4 100644 --- a/lib/transport/http.ex +++ b/lib/transport/http.ex @@ -1,4 +1,4 @@ -defmodule Shortnr.Transport.Http do +defmodule Shortnr.Transport.HTTP do import Plug.Conn @type ok_error :: {:ok, term(), Plug.Conn.t()} | error() diff --git a/lib/transport/json.ex b/lib/transport/json.ex index e8aeed0..cfcd704 100644 --- a/lib/transport/json.ex +++ b/lib/transport/json.ex @@ -1,8 +1,8 @@ defmodule Shortnr.Transport.Json do import Plug.Conn - alias Shortnr.Transport.Http + alias Shortnr.Transport.HTTP - @spec decode_request(Plug.Conn.t(), module()) :: Http.ok_error() + @spec decode_request(Plug.Conn.t(), module()) :: HTTP.ok_error() def decode_request(conn, struct_module) do {:ok, body, conn} = read_body(conn) {:ok, params} = Jason.decode(body) @@ -15,7 +15,7 @@ defmodule Shortnr.Transport.Json do {:ok, struct(struct_module, params_list), conn} end - @spec encode_response(Http.ok_error()) :: Http.ok_error() + @spec encode_response(HTTP.ok_error()) :: HTTP.ok_error() def encode_response({:error, {status, response}, conn}) do {:ok, json_body} = Jason.encode(%{data: response}) {:error, {status, json_body}, conn} diff --git a/lib/transport/text.ex b/lib/transport/text.ex index 7c74bd1..dd00f01 100644 --- a/lib/transport/text.ex +++ b/lib/transport/text.ex @@ -1,15 +1,15 @@ defmodule Shortnr.Transport.Text do import Plug.Conn - alias Shortnr.Transport.Http + alias Shortnr.Transport.HTTP alias Shortnr.URL - @spec decode_request(Plug.Conn.t()) :: Http.ok_error() + @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() + @spec encode_response(HTTP.ok_error()) :: HTTP.ok_error() def encode_response(ok_error = {:error, _, _}), do: ok_error def encode_response({:ok, [], conn}) do diff --git a/lib/url/repo/dets.ex b/lib/url/repo/dets.ex deleted file mode 100644 index 016575e..0000000 --- a/lib/url/repo/dets.ex +++ /dev/null @@ -1,29 +0,0 @@ -defmodule Shortnr.URL.Repo.DETS do - @behaviour Shortnr.URL.Repo - - @impl true - def get(key) do - case :dets.lookup(:urls, key) |> List.first() do - {_, url} -> {:ok, url} - nil -> {:ok, nil} - end - 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 - - @impl true - def delete(key) do - :ok = :dets.delete(:urls, key) - :ok - end -end diff --git a/lib/url/repo/ets.ex b/lib/url/repo/ets.ex new file mode 100644 index 0000000..9820eb6 --- /dev/null +++ b/lib/url/repo/ets.ex @@ -0,0 +1,36 @@ +defmodule Shortnr.URL.Repo.ETS do + @behaviour Shortnr.URL.Repo + + @impl true + def get(key) do + case ets().lookup(:urls, key) |> List.first() do + {_, url} -> {:ok, url} + nil -> {:ok, nil} + end + end + + @impl true + def put(url) do + :ok = ets().insert(:urls, {url.id, url}) + :ok + end + + @impl true + def list() do + resp = ets().select(:urls, [{:"$1", [], [:"$1"]}]) + {:ok, resp |> Enum.map(&elem(&1, 1))} + end + + @impl true + def delete(key) do + :ok = ets().delete(:urls, key) + :ok + end + + @impl true + def reset do + :ok = ets().delete_all_objects(:urls) + end + + defp ets, do: Application.fetch_env!(:shortnr, :ets_implementation) +end diff --git a/lib/url/repo/repo.ex b/lib/url/repo/repo.ex index 48d35d1..c77d46c 100644 --- a/lib/url/repo/repo.ex +++ b/lib/url/repo/repo.ex @@ -6,4 +6,5 @@ defmodule Shortnr.URL.Repo do @callback get(String.t()) :: {:ok, URL.t()} | Transport.error() @callback delete(String.t()) :: :ok | Transport.error() @callback list() :: {:ok, list(URL.t())} | Transport.error() + @callback reset() :: :ok | Transport.error() end diff --git a/lib/url/url.ex b/lib/url/url.ex index 69e721c..47730f3 100644 --- a/lib/url/url.ex +++ b/lib/url/url.ex @@ -10,16 +10,23 @@ defmodule Shortnr.URL do @type t :: %__MODULE__{ id: String.t(), - url: URI.t(), created_at: DateTime.t(), - updated_at: DateTime.t() + updated_at: DateTime.t(), + url: URI.t() } @spec create(String.t(), module()) :: {:ok, String.t()} | Transport.error() def create(url, repo) do url_struct = %URL{id: Util.gen_id(), url: URI.parse(url)} - :ok = repo.put(url_struct) - {:ok, url_struct.id} + + {:ok, extant_url} = repo.get(url_struct.id) + + if is_nil(extant_url) do + :ok = repo.put(url_struct) + {:ok, url_struct.id} + else + create(url, repo) + end end @spec get(String.t(), module()) :: {:ok, URL.t()} | Transport.error() diff --git a/mix.exs b/mix.exs index 1e55f95..558b2db 100644 --- a/mix.exs +++ b/mix.exs @@ -3,7 +3,7 @@ defmodule Shortnr.MixProject do def project do [ - app: :service, + app: :shortnr, version: "0.1.0", elixir: "~> 1.9", start_permanent: Mix.env() == :prod,