about summary refs log tree commit diff
path: root/users/wpcarro/assessments/semiprimes/server/lib
diff options
context:
space:
mode:
Diffstat (limited to 'users/wpcarro/assessments/semiprimes/server/lib')
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/app.ex8
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/cache.ex41
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/extras.ex22
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/math.ex26
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/router.ex86
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/server.ex33
-rw-r--r--users/wpcarro/assessments/semiprimes/server/lib/sup.ex23
7 files changed, 239 insertions, 0 deletions
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/app.ex b/users/wpcarro/assessments/semiprimes/server/lib/app.ex
new file mode 100644
index 0000000000..7a6fa5ea24
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/app.ex
@@ -0,0 +1,8 @@
+defmodule App do
+  use Application
+
+  @impl true
+  def start(_type, _args) do
+    Sup.start_link()
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/cache.ex b/users/wpcarro/assessments/semiprimes/server/lib/cache.ex
new file mode 100644
index 0000000000..cd064cc1ae
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/cache.ex
@@ -0,0 +1,41 @@
+defmodule Cache do
+  @moduledoc """
+  Cache is an in-memory key-value store.
+  """
+  use Agent
+
+  @doc """
+  Inititalize the key-value store.
+  """
+  def start_link(_) do
+    Agent.start_link(fn -> %{} end, name: __MODULE__)
+  end
+
+  @doc """
+  Attempt to return the value stored at `key`
+  """
+  def get(key) do
+    Agent.get(__MODULE__, &Map.get(&1, key))
+  end
+
+  @doc """
+  Write the `value` under the `key`. Last writer wins.
+  """
+  def put(key, value) do
+    Agent.update(__MODULE__, &Map.put(&1, key, value))
+  end
+
+  @doc """
+  List the contents of the cache. Useful for debugging purposes.
+  """
+  def list() do
+    Agent.get(__MODULE__, & &1)
+  end
+
+  @doc """
+  Invalidate the entire cache.
+  """
+  def clear() do
+    Agent.update(__MODULE__, fn _ -> %{} end)
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/extras.ex b/users/wpcarro/assessments/semiprimes/server/lib/extras.ex
new file mode 100644
index 0000000000..f0c2ea4b9e
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/extras.ex
@@ -0,0 +1,22 @@
+defmodule Extras do
+  @moduledoc """
+  Hosts utility functions intended to supplement the standard library.
+  """
+
+  @doc """
+  Return an ascending range starting at `a` and ending at `b` (exclusive).
+
+  ## Examples
+
+      iex> Extras.range(2, 5)
+      [2, 3, 4]
+
+  """
+  def range(a, b) do
+    if b <= a do
+      []
+    else
+      [a] ++ range(a + 1, b)
+    end
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/math.ex b/users/wpcarro/assessments/semiprimes/server/lib/math.ex
new file mode 100644
index 0000000000..8a33be4753
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/math.ex
@@ -0,0 +1,26 @@
+defmodule Math do
+  @moduledoc """
+  Math utilities.
+  """
+  alias Extras
+
+  @doc """
+  Returns the prime factors for `n`.
+
+  ## Examples
+
+      iex> Math.factor(15)
+      [3, 5]
+
+  """
+  def factor(1), do: []
+
+  def factor(n) do
+    Extras.range(2, n - 1)
+    |> Enum.find(&(rem(n, &1) == 0))
+    |> case do
+      nil -> [n]
+      x -> [x | factor(div(n, x))]
+    end
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/router.ex b/users/wpcarro/assessments/semiprimes/server/lib/router.ex
new file mode 100644
index 0000000000..cb55520920
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/router.ex
@@ -0,0 +1,86 @@
+defmodule Router do
+  use Plug.Router
+  use Plug.Debugger
+  require Logger
+
+  plug(Plug.Logger, log: :debug)
+  plug(Plug.Parsers, parsers: [:urlencoded])
+  plug(:match)
+  plug(:dispatch)
+
+  @usage """
+  Usage: Try querying some of the following endpoints...
+    GET /
+    GET /help
+    GET /semiprime?number=<integer>
+    GET /semiprimes?numbers=<comma-separated-integers>
+  """
+
+  get "/" do
+    send_resp(conn, 200, "Welcome to Semiprimes Service!\n\n#{@usage}")
+  end
+
+  get "/help" do
+    send_resp(conn, 200, @usage)
+  end
+
+  get "/semiprime" do
+    case conn |> Map.get(:query_params) |> Map.get("number") do
+      nil ->
+        send_resp(conn, 400, "You must pass an integer as a query parameter. #{@usage}")
+
+      val ->
+        case Integer.parse(val) do
+          {n, ""} ->
+            send_resp(conn, 200, semiprime_response(n))
+
+          _ ->
+            send_resp(conn, 400, "We could not parse the number you provided.\n\n#{@usage}")
+        end
+    end
+  end
+
+  get "/semiprimes" do
+    case conn |> Map.get(:query_params) |> Map.get("numbers") do
+      nil ->
+        send_resp(
+          conn,
+          400,
+          "You must pass a comma-separated list of integers as a query parameter.\n\n#{@usage}"
+        )
+
+      xs ->
+        response =
+          xs
+          |> String.split(",")
+          |> Stream.map(&Integer.parse/1)
+          |> Stream.filter(fn
+            {n, ""} -> true
+            _ -> false
+          end)
+          |> Stream.map(fn {n, ""} -> semiprime_response(n) end)
+          |> Enum.join("\n")
+
+        send_resp(conn, 200, response)
+    end
+  end
+
+  match _ do
+    send_resp(conn, 404, "Not found.")
+  end
+
+  ################################################################################
+  # Utils
+  ################################################################################
+
+  defp semiprime_response(n) do
+    case Server.semiprime(n) do
+      nil ->
+        "#{n} is not a semiprime. Try another number!"
+
+      {hit_or_miss, factors} ->
+        response = "#{n} is a semiprime! Its factors are #{Enum.join(factors, " and ")}."
+        "Cache #{Atom.to_string(hit_or_miss)} - #{response}"
+    end
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/server.ex b/users/wpcarro/assessments/semiprimes/server/lib/server.ex
new file mode 100644
index 0000000000..7ab5e905b5
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/server.ex
@@ -0,0 +1,33 @@
+defmodule Server do
+  @moduledoc """
+  Documentation for `Server`.
+  """
+
+  @doc """
+  If `n` contains exactly two prime factors, return those prime factors;
+  otherwise, return nothing.
+  """
+  def semiprime(n) do
+    case Cache.get(n) do
+      nil ->
+        case do_semiprime(n) do
+          nil ->
+            nil
+
+          res ->
+            Cache.put(n, res)
+            {:miss, res}
+        end
+
+      hit ->
+        {:hit, hit}
+    end
+  end
+
+  defp do_semiprime(n) do
+    case Math.factor(n) do
+      [_, _] = res -> res
+      _ -> nil
+    end
+  end
+end
diff --git a/users/wpcarro/assessments/semiprimes/server/lib/sup.ex b/users/wpcarro/assessments/semiprimes/server/lib/sup.ex
new file mode 100644
index 0000000000..13a6ab374f
--- /dev/null
+++ b/users/wpcarro/assessments/semiprimes/server/lib/sup.ex
@@ -0,0 +1,23 @@
+defmodule Sup do
+  @moduledoc """
+  Top-level supervisor for our OTP application. For now, this supervisor starts
+  and monitors our cache.
+  """
+
+  use Supervisor
+  alias Plug.Adapters.Cowboy
+
+  def start_link(opts \\ []) do
+    Supervisor.start_link(__MODULE__, :ok, opts)
+  end
+
+  @impl true
+  def init(:ok) do
+    children = [
+      Cache,
+      Cowboy.child_spec(scheme: :http, plug: Router, options: [port: 8000])
+    ]
+
+    Supervisor.init(children, strategy: :one_for_one)
+  end
+end