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 process a request before the middleware being described sees it.
-
:expects, a set containing strings or vars identifying other middleware
that must process a request after the middleware being described.
Both :requires and :expects support two kinds of references:
-
Var references (e.g., #'session) create a hard dependency on a specific
middleware. That exact middleware must be present in the stack.
-
String references (e.g., "eval") create a dependency on any middleware
that handles an operation with that name. This is more flexible — it doesn’t
matter which specific middleware provides the op. Prefer string references
when you depend on an operation being available rather than a specific
implementation of it.
-
:handles, a map that documents the operations implemented by the
middleware. Each entry in this map must have as its key the string value of
the handled :op and a value that contains any of four entries:
-
:doc, a human-readable docstring for the middleware
-
:requires, a map of slots that the handled operation must find in request
messages with the indicated :op
-
:optional, a map of slots that the handled operation may utilize from the
request messages with the indicated :op
-
:returns, a map of slots that may be found in messages sent in response
to handling the indicated :op
WARN: Middleware can directly reference other middleware Vars in :requires and
:expects, but all desired middleware must be listed explicitly during handler
construction. In nREPL 1.6+, referencing a middleware Var in :requires or
:expects that is not present in the middleware list will log a warning. The
missing middleware is still auto-included for now, but this deprecated
behavior will be removed in a future release — at that point, missing
dependencies will cause an error. If you see warnings like "Middleware X is
required by Y but is not present in middleware list", add the missing middleware
to your handler or configuration explicitly.
nREPL has no built-in concept of optional middleware dependencies. If a
middleware var is listed in :requires or :expects but is absent from the
stack, a warning is emitted (and the dependency is auto-included, though this
is deprecated).
If your middleware can function without another middleware but should be ordered
relative to it when both are present, use a string reference to an op that
the other middleware handles, rather than a var reference. String references
don’t trigger warnings when no middleware in the stack handles that op — the
ordering constraint is simply ignored during linearization.
;; Hard dependency — warns if wrap-cljs-repl is absent:
{:requires #{#'wrap-cljs-repl}}
;; Orders relative to any middleware that handles the "eval" op for
;; ClojureScript, if present. No warning when absent:
{:requires #{"eval-cljs"}}
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 +docs run -m nrepl.impl.docs here).
|
|
There’s also lein with-profile +docs run -m nrepl.impl.docs --output md
if you’d like to generate an ops listing in Markdown format.
|
Middleware are composed inside-out: the last middleware in the linearized stack
wraps the base handler first, and the first middleware wraps everything else
last. This means the first middleware in the stack is the first to see an
incoming request and the last to see an outgoing response:
Request → [A] → [B] → [C] → base-handler
← Response
If B :requires C → C sees the request before B (C is "inner"/closer to the base)
If B :expects A → B sees the request before A (B is "outer"/further from the base)
In the add-stdin example above, that middleware requires #'session and
expects "eval". This ensures that incoming messages hit the session middleware
first (so that the user’s dynamic scope — including *in* — has been added
to the message) before add-stdin sees them. Additionally, add-stdin must
see messages before 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 other 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. Additionally, default-handler adds
nrepl.middleware/wrap-describe middleware to the stack which provides
describe op support. Any handlers created without using default-handler
(e.g. via direct usage of linearize-middleware-stack to obtain a ordered
sequence of middleware vars) should make sure to add wrap-describe, and also
make a base handler that responds with :unknown-op error to messages with
unrecognized ops.