Skip to content

Instantly share code, notes, and snippets.

@BashkaMen
Created June 25, 2025 16:19
Show Gist options
  • Save BashkaMen/3412cc15306201f1b0dc06e0558ae3bf to your computer and use it in GitHub Desktop.
Save BashkaMen/3412cc15306201f1b0dc06e0558ae3bf to your computer and use it in GitHub Desktop.
defmodule MiddlewareResult do
@type t :: {:pass, Plug.Conn.t()} | :reject
end
defmodule NextMiddleware do
@type t :: (Plug.Conn.t() -> MiddlewareResult.t())
@spec return :: t()
def return, do: fn %Plug.Conn{} = conn -> {:pass, conn} end
@spec skip :: t()
def skip, do: fn %Plug.Conn{} = _conn -> :reject end
end
defmodule Middleware do
@type t :: (Plug.Conn.t(), NextMiddleware.t() -> MiddlewareResult.t())
@spec combine([t()]) :: t()
def combine(middlewares) do
fn %Plug.Conn{} = conn, next ->
next =
middlewares
|> Enum.reverse()
|> Enum.reduce(next, fn middleware, next ->
fn conn -> middleware.(conn, next) end
end)
next.(conn)
end
end
@spec choose([t()]) :: t()
def choose([]), do: NextMiddleware.skip()
def choose([middleware | rest]) do
fn %Plug.Conn{} = conn, next ->
case middleware.(conn, next) do
{:pass, conn} -> {:pass, conn}
:reject -> choose(rest).(conn, next)
end
end
end
@spec method(String.t()) :: t()
def method(name) do
fn %Plug.Conn{} = conn, next ->
if conn.method != name, do: :reject, else: next.(conn)
end
end
@spec route(String.t()) :: t()
def route(path) do
fn %Plug.Conn{} = conn, next ->
if conn.request_path != path, do: :reject, else: next.(conn)
end
end
@spec route(String.t(), String.t()) :: t()
def route(method, path) do
combine([
method(method),
route(path)
])
end
def set_header(key, value) do
fn %Plug.Conn{} = conn, next ->
conn = Plug.Conn.put_resp_header(conn, key, value)
next.(conn)
end
end
def set_code(code) do
fn %Plug.Conn{} = conn, next ->
conn = Plug.Conn.put_status(conn, code)
next.(conn)
end
end
def set_body(body) do
fn %Plug.Conn{} = conn, next ->
next.(%{conn | resp_body: body, state: :set})
end
end
@spec set_json_body(any()) :: t()
def set_json_body(body) do
json = Jason.encode!(body)
combine([
set_header("Content-Type", "application/json"),
set_body(json)
])
end
@spec call(Plug.Conn.t(), Middleware.t()) :: Plug.Conn.t()
def call(%Plug.Conn{} = conn, web_app) do
{:pass, conn} = web_app.(conn, NextMiddleware.return())
Plug.Conn.send_resp(conn)
end
end
defmodule Router do
require Logger
import Middleware
@spec not_found :: Middleware.t()
def not_found(),
do:
combine([
set_header("Content-Type", "text/plain"),
set_code(404),
set_body("Not found")
])
@spec error_handler :: Middleware.t()
def error_handler() do
fn %Plug.Conn{} = conn, next ->
# SUPERVISOR????????
try do
next.(conn)
rescue
e ->
Logger.error("Error: #{inspect(e)}")
combine([
set_code(500),
set_json_body(%{message: "Internal server error"})
]).(conn, NextMiddleware.return())
end
end
end
@spec raw_hello :: Middleware.t()
def raw_hello() do
combine([
method("GET"),
route("/hello"),
set_header("Content-Type", "text/plain"),
set_code(200),
set_body("Hello world")
])
end
@spec html :: Middleware.t()
def html() do
combine([
method("GET"),
route("/html"),
set_header("Content-Type", "text/html"),
set_code(200),
set_body("<div>Hello world</div>")
])
end
@spec error_trigger :: Middleware.t()
def error_trigger() do
combine([
method("GET"),
route("/error"),
fn _conn, _next ->
raise("error, supervisor help me!!!")
end
])
end
def json_handler(handler) do
fn %Plug.Conn{} = conn, next ->
res = handler.(conn)
combine([
set_code(200),
set_json_body(res)
]).(conn, next)
end
end
def create_user() do
combine([
route("GET", "/:create-user"),
json_handler(fn conn ->
%{"name" => name} = conn.params
# todo create user
%{id: "created-id", name: name}
end)
])
end
@behaviour Plug
def init(opts) do
opts
end
def call(conn, _opts) do
Middleware.call(conn, web_app())
end
def web_app() do
combine([
error_handler(),
choose([
# match /hello
raw_hello(),
# match /custom
html(),
error_trigger(),
create_user(),
# everything else to 404
not_found()
])
])
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment