defmodule Nex do @moduledoc """ Nex - A minimalist Elixir web framework powered by HTMX. ## Unified Interface All Nex modules use the same simple statement: defmodule MyApp.Pages.Index do use Nex # ← One statement for everything def render(assigns) do ~H\"\"\"

Hello, Nex!

\"\"\" end end Nex automatically detects the module type based on its path: - `*.Api.*` → API module (no imports needed) - `*.Pages.*` → Page module (imports HEEx + CSRF) - `*.Components.*` → Component module (imports HEEx + CSRF) - `*.Layouts` → Layout module (imports HEEx + CSRF) ## Examples ### Page Module defmodule MyApp.Pages.Users.Index do use Nex def render(assigns) do ~H\"\"\"
Users List
\"\"\" end end ### API Module defmodule MyApp.Api.Users.Index do use Nex def get(req) do Nex.json(%{data: users}) end end ### Component defmodule MyApp.Components.Users.Card do use Nex def render(assigns) do ~H\"\"\"
{@user.name}
\"\"\" end end ### Layout Module defmodule MyApp.Layouts do use Nex def render(assigns) do ~H\"\"\" {raw(@inner_content)} \"\"\" end end """ alias Nex.Response @doc """ Unified macro for all Nex modules. Automatically detects module type based on path and imports appropriate functions. """ defmacro __using__(_opts) do # Get the calling module name at compile time caller_module = __CALLER__.module module_name = caller_module |> Module.split() |> Enum.join(".") cond do # API modules - no imports needed (pure functions) String.contains?(module_name, ".Api.") -> quote do # API modules are pure Elixir modules # No imports needed - just define get/1, post/1, etc. end # Page/Component/Layout modules - need HEEx support String.contains?(module_name, ".Pages.") or String.contains?(module_name, ".Components.") or String.ends_with?(module_name, ".Layouts") -> quote do import Phoenix.Component, only: [sigil_H: 2, render_slot: 1, render_slot: 2] import Phoenix.HTML, only: [raw: 1] import Nex.CSRF, only: [csrf_input_tag: 0, hx_headers: 0, meta_tag: 0, get_token: 0] import Nex.Helpers import Nex.HTMX alias Nex.Cookie alias Nex.Session alias Nex.Flash end # Default: treat as page module true -> quote do import Phoenix.Component, only: [sigil_H: 2, render_slot: 1, render_slot: 2] import Phoenix.HTML, only: [raw: 1] import Nex.CSRF, only: [csrf_input_tag: 0, hx_headers: 0, meta_tag: 0, get_token: 0] import Nex.Helpers import Nex.HTMX alias Nex.Cookie alias Nex.Session alias Nex.Flash end end end # Response type @type json_data :: map() | list() | String.t() @type response_options :: [status: non_neg_integer(), headers: %{String.t() => String.t()}] @doc """ Constructs a JSON response. ## Options * `:status` - HTTP status code (default: 200) * `:headers` - Additional headers (default: %{}) """ @spec json(json_data(), response_options()) :: Response.t() def json(data, opts \\ []) do status = Keyword.get(opts, :status, 200) headers = Keyword.get(opts, :headers, %{}) %Response{ status: status, body: data, headers: headers, content_type: "application/json" } end @doc """ Constructs a text response. """ @spec text(String.t(), response_options()) :: Response.t() def text(text, opts \\ []) do status = Keyword.get(opts, :status, 200) headers = Keyword.get(opts, :headers, %{}) %Response{ status: status, body: text, content_type: "text/plain", headers: headers } end @doc """ Constructs an HTML response. Commonly used in HTMX scenarios to return HTML fragments. ## Examples def get(req) do Nex.html(\"\"\"

User Profile

\"\"\") end ## Options * `:status` - HTTP status code (default: 200) * `:headers` - Additional headers (default: %{}) """ @spec html(String.t(), response_options()) :: Response.t() def html(html, opts \\ []) do status = Keyword.get(opts, :status, 200) headers = Keyword.get(opts, :headers, %{}) %Response{ status: status, body: html, content_type: "text/html", headers: headers } end @doc """ Constructs a redirect response. """ @spec redirect(String.t(), response_options()) :: Response.t() def redirect(to, opts \\ []) do status = Keyword.get(opts, :status, 302) %Response{ status: status, body: "", headers: %{"location" => to}, content_type: "text/html" } end @doc """ Constructs a response with custom status code. """ @spec status(non_neg_integer(), String.t()) :: Response.t() def status(code, body \\ "") do %Response{ status: code, body: body, content_type: "text/plain" } end @doc """ Constructs a Server-Sent Events (SSE) streaming response. Similar to Python's `StreamingResponse` with generators and Next.js's `ReadableStream`. ## Example Nex.stream(fn send -> send.("Thinking...") send.("Processing...") send.("Done!") end) ## Data Formats The `send` function accepts: - String: `send.("Hello")` → `data: Hello\\n\\n` - Map/List: `send.(%{user: "Alice"})` → `data: {"user":"Alice"}\\n\\n` - Event with data: `send.(%{event: "message", data: "Hello"})` → `event: message\\ndata: Hello\\n\\n` ## AI Streaming Example def post(req) do message = req.body["message"] Nex.stream(fn send -> # Stream from OpenAI accumulated = "" Req.post!("https://api.openai.com/v1/chat/completions", json: %{model: "gpt-3.5-turbo", messages: [...], stream: true}, into: fn {:data, chunk}, acc -> case parse_chunk(chunk) do {:ok, content} -> new_acc = acc <> content send.(new_acc) {:cont, new_acc} _ -> {:cont, acc} end end ) end) end """ @spec stream((%{event: String.t(), data: term()} | term() -> term())) :: Response.t() def stream(callback) when is_function(callback, 1) do %Response{ status: 200, content_type: "text/event-stream", body: callback, headers: %{ "cache-control" => "no-cache, no-transform", "connection" => "keep-alive" } } end end