diff options
Diffstat (limited to 'users/wpcarro/assessments/semiprimes/server/lib')
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 000000000000..7a6fa5ea248d --- /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 000000000000..cd064cc1ae4b --- /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 000000000000..f0c2ea4b9e21 --- /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 000000000000..8a33be475389 --- /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 000000000000..cb55520920de --- /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 000000000000..7ab5e905b5a0 --- /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 000000000000..13a6ab374ff6 --- /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 |