It is generally the case that most users of nREPL will expect some minimal REPL
functionality to always be available: evaluation (and the ability to interrupt
evaluations), sessions, file loading, and so on. However, as with all
middleware, the order in which nREPL middleware is applied to a base handler is
significant; e.g., the session middleware’s handler must look up a user’s
session and add it to the message map before delegating to the handler it wraps
(so that e.g. evaluation middleware can use that session data to stand up the
user’s dynamic evaluation context). If middleware were "just" functions, then
any customization of an nREPL middleware stack would need to explicitly repeat
all of the defaults, except for the edge cases where middleware is to be
appended or prepended to the default stack.
To eliminate this tedium, the vars holding nREPL middleware functions may have
a descriptor applied to them to specify certain constraints in how that
middleware is applied. For example, the descriptor for the
nrepl.middleware.session/add-stdin
middleware is set thusly:
(set-descriptor! #'add-stdin
{:requires #{#'session}
:expects #{"eval"}
:handles {"stdin"
{:doc "Add content from the value of \"stdin\" to *in* in the current session."
:requires {"stdin" "Content to add to *in*."}
:optional {}
:returns {"status" "A status of \"need-input\" will be sent if a session's *in* requires content in order to satisfy an attempted read operation."}}}})
Middleware descriptors are implemented as a map in var metadata under a
:nrepl.middleware/descriptor
key. Each descriptor can contain
any of three entries:
-
:requires
, a set containing strings or vars identifying other middleware
that must be applied at a higher level than the middleware being described.
Var references indicate an implementation detail dependency; string values
indicate a dependency on any middleware that handles the specified :op
.
-
:expects
, the same as :requires
, except the referenced middleware must
exist in the final stack at a lower level than the middleware being
described.
|
Another way to think of :expects and :requires would be
before and after. Middleware you’re expecting should have already
been applied by the time the middleware that expects it gets applied,
and middleware that’s required should be applied afterwards. We’ll
expand on this in the paragraphs to come.
|
The values in the :handles
map are used to support the "describe"
operation,
which provides "a machine- and human-readable directory and documentation for
the operations supported by an nREPL endpoint" (see
nrepl.impl.docs/generate-ops-info
and the results of
lein with-profile +maint run -m nrepl.impl.docs
here).
|
There’s also lein with-profile +maint run -m nrepl.impl.docs --output md
if you’d like to generate an ops listing in Markdown format.
|
The :requires
and :expects
entries control the order in which
middleware is applied to a base handler. In the add-stdin
example above,
that middleware will be applied after any middleware that handles the "eval"
operation, but before the nrepl.middleware.session/session
middleware. In the case of add-stdin
, this ensures that incoming messages
hit the session middleware (thus ensuring that the user’s dynamic scope —
including in
— has been added to the message) before the add-stdin
's
handler sees them, so that it may append the provided stdin
content to the
buffer underlying in
. Additionally, add-stdin
must be "above" any eval
middleware, as it takes responsibility for calling clojure.main/skip-if-eol
on in
prior to each evaluation (in order to ensure functional parity with
Clojure’s default stream-based REPL implementation).
The specific contents of a middleware’s descriptor depends entirely on its
objectives: which operations it is to implement/define, how it is to modify
incoming request messages, and which higher- and lower-level middleware are to
aid in accomplishing its aims.
nREPL uses the dependency information in descriptors in order to produce a
linearization of a set of middleware; this linearization is exposed by
nrepl.middleware/linearize-middleware-stack
, which is
implicitly used by nrepl.server/default-handler
to combine the
default stack of middleware with any additional provided middleware vars. The
primary contribution of default-handler
is to use
nrepl.server/unknown-op
as the base handler; this ensures that
unhandled messages will always produce a response message with an :unknown-op
:status
. Any handlers otherwise created (e.g. via direct usage of
linearize-middleware-stack
to obtain a ordered sequence of middleware vars)
should do the same, or use a similar alternative base handler.