defmodule ToweropsWeb.Api.V1.ResourceController do @moduledoc """ Shared helpers for standard API v1 CRUD resource operations. Eliminates the repetitive `ScopedResource.fetch` + case + changeset error handling pattern that appears in every resource controller. ## Usage defmodule ToweropsWeb.Api.V1.SitesController do use ToweropsWeb, :controller alias ToweropsWeb.Api.V1.ResourceController, as: Resources def show(conn, %{"id" => id}) do org_id = conn.assigns.current_organization_id Resources.fetch_and_render(conn, Site, id, org_id, &format_site/1, "Site not found") end end """ alias Phoenix.Controller alias Plug.Conn alias ToweropsWeb.Api.ErrorHelpers alias ToweropsWeb.ScopedResource @doc """ Fetch a resource by id and org scope, render JSON or 404. ## Examples Resources.fetch_and_render(conn, Site, id, org_id, &format_site/1, "Site not found") """ def fetch_and_render(conn, schema, id, org_id, format_fn, not_found_msg) do case ScopedResource.fetch(schema, id, org_id) do {:ok, resource} -> Controller.json(conn, format_fn.(resource)) {:error, :not_found} -> not_found(conn, not_found_msg) end end @doc """ Fetch a resource by id and org scope with preloads, render JSON or 404. """ def fetch_and_render(conn, schema, id, org_id, preloads, format_fn, not_found_msg) do case ScopedResource.fetch_preload(schema, id, org_id, preloads) do {:ok, resource} -> Controller.json(conn, format_fn.(resource)) {:error, :not_found} -> not_found(conn, not_found_msg) end end def update_and_render(conn, schema, id, org_id, opts) do context = opts[:context] update_fn = opts[:update_fn] params = opts[:params] format_fn = opts[:format_fn] not_found_msg = opts[:not_found_msg] case ScopedResource.fetch(schema, id, org_id) do {:ok, resource} -> case apply(context, update_fn, [resource, params]) do {:ok, updated} -> Controller.json(conn, format_fn.(updated)) {:error, %Ecto.Changeset{} = changeset} -> unprocessable_entity(conn, changeset) end {:error, :not_found} -> not_found(conn, not_found_msg) end end @doc """ Fetch a resource, apply a delete function, render success or errors. """ def delete_and_render(conn, schema, id, org_id, context, delete_fn, not_found_msg) do case ScopedResource.fetch(schema, id, org_id) do {:ok, resource} -> case apply(context, delete_fn, [resource]) do {:ok, _} -> Controller.json(conn, %{success: true}) {:error, %Ecto.Changeset{} = changeset} -> unprocessable_entity(conn, changeset) end {:error, :not_found} -> not_found(conn, not_found_msg) end end @doc """ Render 201 created with formatted resource, or 422 with changeset errors. """ def created_or_error(conn, {:ok, resource}, format_fn) do conn |> Conn.put_status(:created) |> Controller.json(format_fn.(resource)) end def created_or_error(conn, {:error, %Ecto.Changeset{} = changeset}, _format_fn) do unprocessable_entity(conn, changeset) end @doc "Render 422 with changeset errors." def unprocessable_entity(conn, changeset) do conn |> Conn.put_status(:unprocessable_entity) |> Controller.json(%{errors: ErrorHelpers.translate_errors(changeset)}) end @doc "Render 404 JSON error." def not_found(conn, message) do conn |> Conn.put_status(:not_found) |> Controller.json(%{error: message}) end @doc "Render 400 for missing required parameter." def missing_param(conn, param_name) do conn |> Conn.put_status(:bad_request) |> Controller.json(%{error: "Missing '#{param_name}' parameter"}) end end