Liking cljdoc? Tell your friends :D

dev.arkaitz.web-base

Public entry point of web-base: the handler that wires the base together and the server lifecycle. A library the host calls; it never calls the host back except through the functions the host hands it (SPEC §3).

The wiring convention IS the product, so here it is, outermost first:

request-id → security headers → proxy (opt-in) → [/wb/ assets → host static → ] session → params → i18n → csrf → subject → ring-handler router, per matched route: error → gate → render → coercion → handler default handler: 404 / 405 / nil-handler 500

Static assets answer before the session: a stylesheet fetch must not mint a session cookie, and a cookie on an asset defeats shared caches. They still carry the request id and the security headers. Session sits outside subject (the subject function reads the session) and outside csrf (the token lives in the session); params sits outside csrf because the token may arrive as a form field; i18n sits outside csrf so a refused request's error page speaks the negotiated language. Error sits outside gate and render inside the router: a predicate or a layout may throw, and a refusal is an error datum. The default handler runs outside router middleware, so the request id, the session and the security headers reach it from the outer stack while errors render on their own.

Public entry point of web-base: the handler that wires the base together and
the server lifecycle. A library the host calls; it never calls the host back
except through the functions the host hands it (SPEC §3).

The wiring convention IS the product, so here it is, outermost first:

  request-id → security headers → proxy (opt-in)
  → [/wb/ assets → host static → ] session → params → i18n → csrf → subject
  → ring-handler
      router, per matched route: error → gate → render → coercion → handler
      default handler: 404 / 405 / nil-handler 500

Static assets answer before the session: a stylesheet fetch must not mint a
session cookie, and a cookie on an asset defeats shared caches. They still
carry the request id and the security headers. Session sits outside subject
(the subject function reads the session) and outside csrf (the token lives
in the session); params sits outside csrf because the token may arrive as a
form field; i18n sits outside csrf so a refused request's error page speaks
the negotiated language. Error sits outside gate and render inside the
router: a predicate or a layout may throw, and a refusal is an error datum.
The default handler runs outside router middleware, so the request id, the
session and the security headers reach it from the outer stack while errors
render on their own.
raw docstring

dev.arkaitz.web-base.config

EDN configuration, read explicitly and passed in (SPEC §3, §6). The one reader tag the base adds, #wb/env "VAR", is how a secret such as the session key reaches the config from the environment without ever being committed. It has no default form on purpose: a default is how a development key reaches production (SPEC §11).

A development machine may name the variables in a file instead of exporting them on every start: env-file-readers. That file is asked only for what the environment does not have, it is never looked for on its own, and what comes out of it is logged.

EDN configuration, read explicitly and passed in (SPEC §3, §6). The one
reader tag the base adds, `#wb/env "VAR"`, is how a secret such as the
session key reaches the config from the environment without ever being
committed. It has no default form on purpose: a default is how a
development key reaches production (SPEC §11).

A development machine may name the variables in a file instead of exporting
them on every start: `env-file-readers`. That file is asked only for what the
environment does not have, it is never looked for on its own, and what comes
out of it is logged.
raw docstring

dev.arkaitz.web-base.error

Errors as data with three renderings (SPEC §9): a full page, a fragment for an htmx swap, or plain text when the client does not accept HTML. A datum is {:status n} plus optional :title and :detail strings; a coercion failure adds :wb/coercion, the humanized explanation as data. The base's own datums carry a status and structured data, never prose — it knows no language (SPEC §14); words come from the host's :error-layout, which receives the request and with it whatever the host put there.

The default error page is self-contained on purpose: a layout that just threw must not be asked to render its own failure.

Errors as data with three renderings (SPEC §9): a full page, a fragment for
an htmx swap, or plain text when the client does not accept HTML. A datum is
`{:status n}` plus optional `:title` and `:detail` strings; a coercion
failure adds `:wb/coercion`, the humanized explanation as data. The base's
own datums carry a status and structured data, never prose — it knows no
language (SPEC §14); words come from the host's `:error-layout`, which
receives the request and with it whatever the host put there.

The default error page is self-contained on purpose: a layout that just
threw must not be asked to render its own failure.
raw docstring

dev.arkaitz.web-base.gate

The gate on private pages (SPEC §5, §9). The base knows that a request may carry a subject and nothing about what a subject is: the host hands it a function from request to subject-or-nil, and per route a predicate over the request. What the base owns is the translation of a refusal into the right outcome for the kind of request — a 303 for a navigation, so a refused POST is never re-posted to the login page; an HX-Redirect for an htmx swap so the login page never lands inside a div; and a 403 error datum when there is a subject and the predicate still says no. No 401: a proper one needs WWW-Authenticate, and only whoever authenticates knows the scheme.

The gate on private pages (SPEC §5, §9). The base knows that a request may
carry a subject and nothing about what a subject is: the host hands it a
function from request to subject-or-nil, and per route a predicate over the
request. What the base owns is the translation of a refusal into the right
outcome for the kind of request — a `303` for a navigation, so a refused
POST is never re-posted to the login page; an `HX-Redirect` for an htmx swap
so the login page never lands inside a `div`; and a 403 error datum when
there is a subject and the predicate still says no. No `401`: a proper one
needs `WWW-Authenticate`, and only whoever authenticates knows the scheme.
raw docstring

dev.arkaitz.web-base.htmx

The named places where web-base touches htmx (SPEC §9). Every HX- literal in the base lives in this file: the request classifier that decides fragment or page, and the redirect response the gate answers with. The error renderer is the third place and it calls partial-request? rather than reading headers.

Verified against htmx 4.0.0: HX-Request is sent on every htmx request, and HX-Request-Type says full when the swap targets the whole document — a history restore, a body-targeted swap — and partial otherwise. Deciding on HX-Request alone would hand a bare fragment to the back button.

The named places where web-base touches htmx (SPEC §9). Every `HX-` literal in
the base lives in this file: the request classifier that decides fragment or
page, and the redirect response the gate answers with. The error renderer is
the third place and it calls `partial-request?` rather than reading headers.

Verified against htmx 4.0.0: `HX-Request` is sent on every htmx request, and
`HX-Request-Type` says `full` when the swap targets the whole document — a
history restore, a body-targeted swap — and `partial` otherwise. Deciding on
`HX-Request` alone would hand a bare fragment to the back button.
raw docstring

dev.arkaitz.web-base.i18n

Which language to render, per request (SPEC §14). The host gives a Tempura dictionary — its top-level keys are the supported locales — a default locale, and optionally a function returning the request's own preference list (a choice kept in the session, a cookie, a URL prefix). The base puts two things on the request: :wb/tr, Tempura's translate function bound to the preferences, and :wb/locale, the first preference the dictionary actually supports, so <html lang> never names a language the page is not rendered in.

Read from the request every time, never cached per session: the htmx fragment that arrives a second later carries the same headers and must land in the same language.

Which language to render, per request (SPEC §14). The host gives a Tempura
dictionary — its top-level keys are the supported locales — a default
locale, and optionally a function returning the request's own preference
list (a choice kept in the session, a cookie, a URL prefix). The base puts
two things on the request: `:wb/tr`, Tempura's translate function bound to
the preferences, and `:wb/locale`, the first preference the dictionary
actually supports, so `<html lang>` never names a language the page is not
rendered in.

Read from the request every time, never cached per session: the htmx
fragment that arrives a second later carries the same headers and must land
in the same language.
raw docstring

dev.arkaitz.web-base.integrant

Optional: Integrant methods for a host that uses it (SPEC §10). The only namespace of the base that requires integrant; nothing else depends on it, and a host without Integrant calls dev.arkaitz.web-base/handler and start itself.

A config for ::handler holds functions — routes, the subject function, layouts — which EDN cannot express, so a host writes it as an Integrant component of its own and refers to it, or builds the map in code:

{::wb/handler {:routes #ig/ref :my/routes :session {...} ...}
 ::wb/server  {:handler #ig/ref ::wb/handler :port 3000}}

config/readers is merged in so #wb/env works in the same EDN, and the two-argument form takes readers of the host's own.

Optional: Integrant methods for a host that uses it (SPEC §10). The only
namespace of the base that requires integrant; nothing else depends on it,
and a host without Integrant calls `dev.arkaitz.web-base/handler` and
`start` itself.

A config for `::handler` holds functions — routes, the subject function,
layouts — which EDN cannot express, so a host writes it as an Integrant
component of its own and refers to it, or builds the map in code:

    {::wb/handler {:routes #ig/ref :my/routes :session {...} ...}
     ::wb/server  {:handler #ig/ref ::wb/handler :port 3000}}

`config/readers` is merged in so `#wb/env` works in the same EDN, and the
two-argument form takes readers of the host's own.
raw docstring

dev.arkaitz.web-base.log

A request id threaded through one request (SPEC §6): on the request as :wb/request-id, on the response as X-Request-Id, in the SLF4J MDC as request-id for the duration, and in one access line. The base ships no logging backend — a library must not — so the line goes through tools.logging and the MDC key reaches whatever pattern the host configures (%X{request-id} in logback).

An incoming X-Request-Id is not trusted: a client could then choose what the logs say. Behind a proxy that assigns ids, the host maps them.

A request id threaded through one request (SPEC §6): on the request as
`:wb/request-id`, on the response as `X-Request-Id`, in the SLF4J MDC as
`request-id` for the duration, and in one access line. The base ships no
logging backend — a library must not — so the line goes through
tools.logging and the MDC key reaches whatever pattern the host configures
(`%X{request-id}` in logback).

An incoming `X-Request-Id` is not trusted: a client could then choose what
the logs say. Behind a proxy that assigns ids, the host maps them.
raw docstring

dev.arkaitz.web-base.native

Optional, like the Integrant namespace: requiring it is the opt-in. It teaches Ring to read a classpath resource from inside a GraalVM native image, where resources are served under the resource: URL protocol that ring.util.response/resource-data knows nothing about — so without this every asset the base serves, its own CSS and htmx included, answers 500.

The method is inert outside an image: no JVM produces that protocol, and only the tests reach the body, through a URL carrying a handler of their own. The build flags an image needs are in the README, not here: they move between GraalVM releases, and a stale flag in a README is visible while a stale flag compiled into a library is a build that breaks for a reason its user cannot see.

Two things this is not, both unlike the Integrant namespace it otherwise mirrors. :resource is a bare key on a multimethod that belongs to Ring, not a namespaced key of ours on a multimethod of ours, so whoever loads last wins if Ring or another library ever defines it — which is what the tripwire in the tests watches for. And requiring is the opt-in only for whoever requires: a host library that requires this hands the method to its own consumers, who never asked.

Optional, like the Integrant namespace: requiring it is the opt-in. It
teaches Ring to read a classpath resource from inside a GraalVM native
image, where resources are served under the `resource:` URL protocol that
`ring.util.response/resource-data` knows nothing about — so without this
every asset the base serves, its own CSS and htmx included, answers 500.

The method is inert outside an image: no JVM produces that protocol, and
only the tests reach the body, through a URL carrying a handler of their
own. The build flags an image needs are in the README, not here: they move
between GraalVM releases, and a stale flag in a README is visible while a
stale flag compiled into a library is a build that breaks for a reason its
user cannot see.

Two things this is not, both unlike the Integrant namespace it otherwise
mirrors. `:resource` is a bare key on a multimethod that belongs to Ring,
not a namespaced key of ours on a multimethod of ours, so whoever loads
last wins if Ring or another library ever defines it — which is what the
tripwire in the tests watches for. And requiring is the opt-in only for
whoever requires: a host library that requires this hands the method to
its own consumers, who never asked.
raw docstring

No vars found in this namespace.

dev.arkaitz.web-base.render

Turns a handler's Hiccup body into HTML, applying the route's layout stack from the height the request asks for (SPEC §12).

A layout is a function of one map of slots — :content, :request and whatever the response put under :wb/slots — and nesting is composition: the stack folds from the innermost layout outwards. Route data :wb/layouts is a vector, outermost first; reitit concatenates a parent's vector with its children's, so nested routes accumulate their stack and a child opts out with ^:replace. Height counts from the innermost layout, which is why adding an outer layout later never invalidates a height a handler already names.

Turns a handler's Hiccup body into HTML, applying the route's layout stack
from the height the request asks for (SPEC §12).

A layout is a function of one map of slots — `:content`, `:request` and
whatever the response put under `:wb/slots` — and nesting is composition:
the stack folds from the innermost layout outwards. Route data `:wb/layouts`
is a vector, outermost first; reitit concatenates a parent's vector with its
children's, so nested routes accumulate their stack and a child opts out with
`^:replace`. Height counts from the innermost layout, which is why adding an
outer layout later never invalidates a height a handler already names.
raw docstring

dev.arkaitz.web-base.response

Constructors for the responses a handler writes most: the Ring maps the base reads, with nothing else in them. ok carries Hiccup for the route's layouts; see-other is the redirect after a classic form. There is no not-found or forbidden here on purpose: those are error/throw!, so they reach the error renderer and not the layout stack.

Constructors for the responses a handler writes most: the Ring maps the
base reads, with nothing else in them. `ok` carries Hiccup for the route's
layouts; `see-other` is the redirect after a classic form. There is no
`not-found` or `forbidden` here on purpose: those are `error/throw!`, so
they reach the error renderer and not the layout stack.
raw docstring

dev.arkaitz.web-base.security

Web security measures that are neither authentication nor authorisation (SPEC §15): response headers added when absent, the host's Content Security Policy with a per-request nonce, opt-in trust of a reverse proxy's headers, and CSRF protection through ring-anti-forgery's synchroniser token.

Written down so it is not rediscovered: htmx's hx-on, hx-vals js: and trigger filters need unsafe-eval or the hx-csp extension; a strict policy means doing without them, which the demo does.

Web security measures that are neither authentication nor authorisation
(SPEC §15): response headers added when absent, the host's Content Security
Policy with a per-request nonce, opt-in trust of a reverse proxy's headers,
and CSRF protection through ring-anti-forgery's synchroniser token.

Written down so it is not rediscovered: htmx's `hx-on`, `hx-vals js:` and
trigger filters need `unsafe-eval` or the `hx-csp` extension; a strict
policy means doing without them, which the demo does.
raw docstring

dev.arkaitz.web-base.server

Jetty behind two functions. Jetty lives entirely inside the base (SPEC §10): the host sees a handle map, never the server class. The bound port is in the handle because with :port 0 there is no other way to learn it without Jetty's own API.

Jetty behind two functions. Jetty lives entirely inside the base (SPEC
§10): the host sees a handle map, never the server class. The bound port is
in the handle because with `:port 0` there is no other way to learn it
without Jetty's own API.
raw docstring

dev.arkaitz.web-base.session

Sessions over Ring's store port (SPEC §11). The host gives {:store s} — any ring.middleware.session.store/SessionStore — or {:key k}, which selects Ring's cookie store with that signing key. Nothing else: the base never generates a key at startup, because a generated key destroys every session on every deploy and differs per instance, and it does so without a symptom.

Two things the cookie store cannot do, so the host knows what it is choosing: a cookie session cannot be revoked from the server, and its sealed payload carries no timestamp, so the cookie's Max-Age is the only expiry there is.

Sessions over Ring's store port (SPEC §11). The host gives `{:store s}` — any
`ring.middleware.session.store/SessionStore` — or `{:key k}`, which selects
Ring's cookie store with that signing key. Nothing else: the base never
generates a key at startup, because a generated key destroys every session
on every deploy and differs per instance, and it does so without a symptom.

Two things the cookie store cannot do, so the host knows what it is choosing:
a cookie session cannot be revoked from the server, and its sealed payload
carries no timestamp, so the cookie's `Max-Age` is the only expiry there is.
raw docstring

dev.arkaitz.web-base.shell

The page shell as a set of slots the host fills (SPEC §13): :lang, :title, :head, :header, :nav, :identity, :content, :footer. It is an ordinary layout — a function of one slot map — usable as the outermost entry of a route's :wb/layouts or called by the host's own layout. Absent slots emit nothing.

What the shell reads from :request, when the request carries it: the negotiated locale for lang (SPEC §14), the CSP nonce for its own script tag and the CSRF token, sent on every htmx request as X-CSRF-Token through hx-headers:inherited on <body> (SPEC §15) — htmx 4 inherits nothing without the modifier. A <script> the host adds through :head needs its own nonce attribute, read from (:wb/nonce request).

The page shell as a set of slots the host fills (SPEC §13): `:lang`,
`:title`, `:head`, `:header`, `:nav`, `:identity`, `:content`, `:footer`.
It is an ordinary layout — a function of one slot map — usable as the
outermost entry of a route's `:wb/layouts` or called by the host's own
layout. Absent slots emit nothing.

What the shell reads from `:request`, when the request carries it: the
negotiated locale for `lang` (SPEC §14), the CSP nonce for its own script
tag and the CSRF token, sent on every htmx request as `X-CSRF-Token`
through `hx-headers:inherited` on `<body>` (SPEC §15) — htmx 4 inherits
nothing without the modifier. A `<script>` the host adds through `:head`
needs its own `nonce` attribute, read from `(:wb/nonce request)`.
raw docstring

dev.arkaitz.web-base.testing

Helpers for a host's own test suite, over plain Ring maps and rendered bodies. Nothing here needs ring-mock or any other library, so it ships in the jar. The shapes parsed are the base's own — the cookies Ring's session middleware sets, the CSRF token the shell and security/csrf-field emit — which is why the base owns these readers rather than every host's tests.

Helpers for a host's own test suite, over plain Ring maps and rendered
bodies. Nothing here needs ring-mock or any other library, so it ships in
the jar. The shapes parsed are the base's own — the cookies Ring's session
middleware sets, the CSRF token the shell and `security/csrf-field` emit —
which is why the base owns these readers rather than every host's tests.
raw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close