Skip to main content

lib/core/http/router.ex

# lib/core/http/router.ex
defmodule Core.HTTP.Router do
  @moduledoc """
  Default router implementation.
  This is used by the demo application.
  """
  use Plug.Router
  require Logger
  alias Core.Workers.JobQueue

  plug Plug.Logger, log: :info
  plug :match
  plug Plug.Parsers, parsers: [:json], pass: ["application/json"], json_decoder: Jason
  plug Plug.Telemetry, event_prefix: [:server, :http]
  plug :dispatch

  # Root endpoint
  get "/" do
    send_resp(conn, 200, "Server is running")
  end

  # Health check
  get "/health" do
    worker_alive = Process.whereis(JobQueue) != nil
    {status, code} = 
      if worker_alive, 
        do: {"OK", 200}, 
        else: {"DEGRADED", 503}

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(code, Jason.encode!(%{status: status}))
  end

  get "/stats" do 
    stats = JobQueue.stats()

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(200, Jason.encode!(stats))
  end

  # Submit a new job
  post "/jobs" do
    case conn.body_params do
      %{"payload" => payload}  when is_map(payload) ->
        opts = []
        opts = if conn.body_params["max_attempts"], do: Keyword.put(opts, :max_attempts, conn.body_params["max_attempts"]), else: opts

        {:ok, id} = JobQueue.submit(payload, opts)

        conn
        |> put_resp_content_type("application/json")
        |> send_resp(202, Jason.encode!(%{message: "Job accepted", job_id: id}))
 
      %{"payload" => _} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(400, Jason.encode!(%{error: "'payload' must be a JSON object"}))
 
      _ ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(400, Jason.encode!(%{error: "Missing 'payload' field"}))
    end
  end

  post "/jobs/schedule" do
    with %{"payload" => payload, "run_at" => run_at_str} <- conn.body_params,
         {:ok, run_at, _} <- DateTime.from_iso8601(run_at_str) do
      {:ok, id} = JobQueue.submit_at(payload, run_at)
 
      conn
      |> put_resp_content_type("application/json")
      |> send_resp(202, Jason.encode!(%{message: "Job scheduled", job_id: id, run_at: run_at_str}))
    else
      _ ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(400, Jason.encode!(%{error: "Required fields: payload (object), run_at (ISO8601)"}))
    end
  end

  get "/jobs" do
    try do 
    params = conn.query_params
 
    opts =
      []
      |> maybe_put(:status, params["status"] && String.to_existing_atom(params["status"]))
      |> maybe_put(:page, params["page"] && String.to_integer(params["page"]))
      |> maybe_put(:per_page, params["per_page"] && String.to_integer(params["per_page"]))
 
    jobs = JobQueue.all(opts)
 
    conn
    |> put_resp_content_type("application/json")
    |> send_resp(200, Jason.encode!(jobs))
  rescue
    ArgumentError ->
      conn
      |> put_resp_content_type("application/json")
      |> send_resp(400, Jason.encode!(%{error: "Invalid status filter. Valid values: queued, running, done, failed"}))
    end
  end
 
  get "/jobs/:id" do
    with {int_id, ""} <- Integer.parse(id),
         {:ok, job} <- JobQueue.get(int_id) do
      conn
      |> put_resp_content_type("application/json")
      |> send_resp(200, Jason.encode!(job))
    else
      :error ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(400, Jason.encode!(%{error: "Job ID must be an integer"}))
 
      {:error, :not_found} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(404, Jason.encode!(%{error: "Job not found"}))
    end
  end
 
  match _ do
    send_resp(conn, 404, Jason.encode!(%{error: "Not found"}))
  end

  defp maybe_put(opts, _key, nil), do: opts
  defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value)
end